File Upload
Vue formDrag-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
$ pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/file-upload.json$ npx shadcn-vue@latest add https://uipkge.dev/r/vue/file-upload.json$ yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/file-upload.json$ bunx 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