UIPackage

File Upload

Vue form
Edit on GitHub

Drag-and-drop file dropzone with click-to-browse fallback, file-type filtering, multi-file support, and per-file progress + remove controls. Wraps native `<input type="file">` with proper a11y.

Also available for React ->

Installation

$ npx shadcn-vue@latest add https://uipkge.dev/r/vue/file-upload.json

Or with the named registry: npx shadcn-vue@latest add @uipkge/file-upload

Examples

Props

Name Type / Values Default Required
class HTMLAttributes['class'] optional
accept string optional
multiple boolean optional
disabled boolean optional
modelValue File[] optional

Dependencies

Files (7)

  • app/components/ui/file-upload/FileUpload.vue 2.5 kB
    <script setup lang="ts">
    import { ref, type HTMLAttributes } from 'vue'
    import { cn } from '@/lib/utils'
    
    const props = defineProps<{
      class?: HTMLAttributes['class']
      accept?: string
      multiple?: boolean
      disabled?: boolean
      modelValue?: File[]
    }>()
    
    const emit = defineEmits<{
      'update:modelValue': [files: File[]]
    }>()
    
    const inputRef = ref<HTMLInputElement>()
    const isDragging = ref(false)
    
    function handleFiles(files: FileList | null) {
      if (!files) return
      const fileArray = Array.from(files)
      const first = fileArray[0]
      emit('update:modelValue', props.multiple ? fileArray : first ? [first] : [])
    }
    
    function handleInputChange(e: Event) {
      handleFiles((e.target as HTMLInputElement).files)
    }
    
    function handleDrop(e: DragEvent) {
      isDragging.value = false
      handleFiles(e.dataTransfer?.files ?? null)
    }
    
    function handleDragOver(e: DragEvent) {
      e.preventDefault()
      isDragging.value = true
    }
    
    function handleDragLeave() {
      isDragging.value = false
    }
    
    function openFilePicker() {
      inputRef.value?.click()
    }
    </script>
    
    <template>
      <div :class="cn('space-y-3', props.class)" v-bind="$attrs">
        <input
          ref="inputRef"
          type="file"
          :accept="accept"
          :multiple="multiple"
          :disabled="disabled"
          class="sr-only"
          @change="handleInputChange"
        />
    
        <div
          :class="[
            'border-muted-foreground/25 hover:border-muted-foreground/50 bg-muted/50 flex flex-col items-center justify-center rounded-lg border-2 border-dashed p-8 transition-colors duration-200',
            isDragging && 'border-primary bg-primary/5',
            disabled && 'pointer-events-none opacity-50',
          ]"
          @click="openFilePicker"
          @drop.prevent="handleDrop"
          @dragover="handleDragOver"
          @dragleave="handleDragLeave"
        >
          <slot name="icon">
            <svg
              class="text-muted-foreground mb-2 size-10"
              fill="none"
              stroke="currentColor"
              stroke-width="1.5"
              viewBox="0 0 24 24"
            >
              <path
                stroke-linecap="round"
                stroke-linejoin="round"
                d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"
              />
            </svg>
          </slot>
          <slot>
            <p class="text-muted-foreground text-sm">
              <span class="text-foreground font-semibold">Click to upload</span> or drag and drop
            </p>
            <p v-if="accept" class="text-muted-foreground/70 mt-1 text-xs">{{ accept }}</p>
          </slot>
        </div>
    
        <slot name="content" />
      </div>
    </template>
  • app/components/ui/file-upload/FileUploadContent.vue 0.3 kB
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue'
    import { cn } from '@/lib/utils'
    
    const props = defineProps<{
      class?: HTMLAttributes['class']
    }>()
    </script>
    
    <template>
      <div :class="cn('space-y-2', props.class)" v-bind="$attrs">
        <slot />
      </div>
    </template>
  • app/components/ui/file-upload/FileUploadItem.vue 1 kB
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue'
    import { FileIcon, X } from 'lucide-vue-next'
    import { cn } from '@/lib/utils'
    
    const props = defineProps<{
      class?: HTMLAttributes['class']
    }>()
    
    const file = defineModel<File>({ required: true })
    
    defineEmits<{
      (e: 'remove'): void
    }>()
    </script>
    
    <template>
      <div :class="cn('bg-muted/50 flex items-center gap-3 rounded-md border p-3', props.class)">
        <FileIcon class="text-muted-foreground size-8 shrink-0" />
        <div class="min-w-0 flex-1">
          <p class="truncate text-sm font-medium">{{ file.name }}</p>
          <p class="text-muted-foreground text-xs">{{ (file.size / 1024).toFixed(1) }} KB</p>
        </div>
        <button
          type="button"
          class="text-muted-foreground hover:text-foreground focus-visible:ring-ring ml-auto rounded-sm transition-colors duration-200 focus-visible:ring-1 focus-visible:outline-none"
          @click="$emit('remove')"
        >
          <X class="size-4" aria-hidden="true" />
          <span class="sr-only">Remove file</span>
        </button>
      </div>
    </template>
  • app/components/ui/file-upload/FileUploadItemName.vue 0.3 kB
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue'
    import { cn } from '@/lib/utils'
    
    const props = defineProps<{
      class?: HTMLAttributes['class']
    }>()
    </script>
    
    <template>
      <p :class="cn('truncate text-sm font-medium', props.class)" v-bind="$attrs">
        <slot />
      </p>
    </template>
  • app/components/ui/file-upload/FileUploadItemSize.vue 0.3 kB
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue'
    import { cn } from '@/lib/utils'
    
    const props = defineProps<{
      class?: HTMLAttributes['class']
    }>()
    </script>
    
    <template>
      <span :class="cn('text-muted-foreground text-xs', props.class)" v-bind="$attrs">
        <slot />
      </span>
    </template>
  • app/components/ui/file-upload/FileUploadTrigger.vue 0.3 kB
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue'
    import { cn } from '@/lib/utils'
    
    const props = defineProps<{
      class?: HTMLAttributes['class']
    }>()
    </script>
    
    <template>
      <div :class="cn('cursor-pointer', props.class)" v-bind="$attrs">
        <slot />
      </div>
    </template>
  • app/components/ui/file-upload/index.ts 0.4 kB
    export { default as FileUpload } from './FileUpload.vue'
    export { default as FileUploadTrigger } from './FileUploadTrigger.vue'
    export { default as FileUploadContent } from './FileUploadContent.vue'
    export { default as FileUploadItem } from './FileUploadItem.vue'
    export { default as FileUploadItemName } from './FileUploadItemName.vue'
    export { default as FileUploadItemSize } from './FileUploadItemSize.vue'

Raw manifest: https://uipkge.dev/r/vue/file-upload.json