Skip to main content
The DUploadAvatar component provides a complete avatar upload experience with image preview, file selection, and customizable upload button. It handles both new uploads and existing images.

Props

src
string
default:""
The source URL of the previously uploaded avatar image. Displayed as the initial preview.
buttonSize
ButtonSize
default:"sm"
The size of the upload/change button
buttonColor
ButtonColor
default:"neutral"
The color variant of the upload/change button

Events

on-upload-image
(file: File) => void
Emitted when an image file is selected for upload. Receives the File object as parameter.

Usage

Basic Avatar Upload

<template>
  <DUploadAvatar @on-upload-image="handleUpload" />
</template>

<script setup>
function handleUpload(file: File) {
  console.log('File selected:', file.name)
  // Upload logic here
}
</script>

With Existing Avatar

<template>
  <DUploadAvatar
    :src="user.avatarUrl"
    @on-upload-image="handleUpload"
  />
</template>

<script setup>
const user = ref({
  avatarUrl: '/images/user-avatar.jpg'
})

function handleUpload(file: File) {
  // Upload to server and update user.avatarUrl
}
</script>

Custom Button Styling

<template>
  <DUploadAvatar
    button-size="md"
    button-color="primary"
    @on-upload-image="handleUpload"
  />
</template>

Complete Profile Form

<template>
  <form @submit.prevent="handleSubmit">
    <div class="space-y-6">
      <div>
        <label class="block text-sm font-medium mb-2">
          Profile Picture
        </label>
        <DUploadAvatar
          :src="formData.avatar"
          @on-upload-image="handleAvatarUpload"
        />
      </div>
      
      <UFormField label="Name" name="name">
        <UInput v-model="formData.name" />
      </UFormField>
      
      <DActionButtons
        primary-button-text="Save Profile"
        secondary-button-text="Cancel"
        @on-click-primary-button="handleSubmit"
      />
    </div>
  </form>
</template>

<script setup>
const formData = reactive({
  avatar: '/images/default-avatar.jpg',
  name: ''
})

function handleAvatarUpload(file: File) {
  // Create object URL for immediate preview
  formData.avatar = URL.createObjectURL(file)
  
  // Upload to server
  uploadToServer(file)
}

function uploadToServer(file: File) {
  const formData = new FormData()
  formData.append('avatar', file)
  
  // API call
  fetch('/api/upload-avatar', {
    method: 'POST',
    body: formData
  })
}
</script>

With Loading State

<template>
  <div class="relative">
    <DUploadAvatar
      :src="avatarUrl"
      :button-color="isUploading ? 'neutral' : 'primary'"
      @on-upload-image="handleUpload"
    />
    <div v-if="isUploading" class="absolute inset-0 bg-black/50 flex items-center justify-center rounded-md">
      <UIcon name="i-lucide-loader-2" class="animate-spin text-white" />
    </div>
  </div>
</template>

<script setup>
const isUploading = ref(false)
const avatarUrl = ref('')

async function handleUpload(file: File) {
  isUploading.value = true
  
  try {
    const url = await uploadFile(file)
    avatarUrl.value = url
  } finally {
    isUploading.value = false
  }
}
</script>

With File Validation

<template>
  <div>
    <DUploadAvatar @on-upload-image="handleUpload" />
    <p v-if="error" class="text-error text-sm mt-2">{{ error }}</p>
  </div>
</template>

<script setup>
const error = ref('')

function handleUpload(file: File) {
  error.value = ''
  
  // Validate file size (2MB max)
  if (file.size > 2 * 1024 * 1024) {
    error.value = 'File size must be less than 2MB'
    return
  }
  
  // Validate file type
  if (!file.type.startsWith('image/')) {
    error.value = 'File must be an image'
    return
  }
  
  // Proceed with upload
  uploadFile(file)
}
</script>

Multiple Upload Formats

<template>
  <div class="grid grid-cols-2 gap-4">
    <div>
      <h3 class="text-sm font-medium mb-2">Profile Photo</h3>
      <DUploadAvatar
        :src="photos.profile"
        button-size="sm"
        @on-upload-image="(file) => handleUpload(file, 'profile')"
      />
    </div>
    
    <div>
      <h3 class="text-sm font-medium mb-2">Cover Photo</h3>
      <DUploadAvatar
        :src="photos.cover"
        button-size="sm"
        @on-upload-image="(file) => handleUpload(file, 'cover')"
      />
    </div>
  </div>
</template>

<script setup>
const photos = reactive({
  profile: '',
  cover: ''
})

function handleUpload(file: File, type: 'profile' | 'cover') {
  photos[type] = URL.createObjectURL(file)
  // Upload logic
}
</script>

Implementation Details

Image Preview

The component uses computed property to determine which image to show:
const getImage = computed(() => {
  if (fileName.value) {
    // Show newly selected file
    const imgUrl = globalThis.URL.createObjectURL(fileName.value)
    return imgUrl
  } else if (props.src) {
    // Show existing image
    return props.src
  }
  return null
})

File Input Handling

  • Hidden file input with type="file" and accept="image/*"
  • Triggered programmatically via template ref
  • Handles both regular file selection and drag-and-drop events

Default State

When no image is available:
  • Displays a placeholder with photo icon
  • Clickable area triggers file selection
  • Primary color background (bg-primary-50)

Styling

  • Avatar Preview: size-20 (80px) rounded square with object-cover
  • Button Position: Aligned to bottom with items-end
  • Layout: Horizontal flex with gap-x-6
  • Button Text: “Cambiar” (Change in Spanish)

Type Exports

export type ButtonSize = InstanceType<typeof UButton>['$props']['size']
export type ButtonColor = InstanceType<typeof UButton>['$props']['color']
Source: /home/daytona/workspace/source/app/components/d/upload/d-upload-avatar.vue:73

Build docs developers (and LLMs) love