FileUpload
Usage
Use the v-model directive to control the value of the FileUpload.
<script setup lang="ts">
const value = ref(null)
</script>
<template>
<PFileUpload v-model="value" class="w-96 min-h-48" />
</template>
Multiple
Use the multiple prop to allow multiple files to be selected.
<template>
<PFileUpload multiple class="w-96 min-h-48" />
</template>
Dropzone
Use the dropzone prop to enable/disable the droppable area. Defaults to true.
<template>
<PFileUpload :dropzone="false" class="w-96 min-h-48" />
</template>
Interactive
Use the interactive prop to enable/disable the clickable area. Defaults to true.
<template>
<PFileUpload :interactive="false" class="w-96 min-h-48" />
</template>
Accept
Use the accept prop to specify the allowed file types for the input. Provide a comma-separated list of MIME types or file extensions (e.g., image/png,application/pdf,.jpg). Defaults to * (all file types).
<template>
<PFileUpload accept="image/*" class="w-96 min-h-48" />
</template>
Label
Use the label prop to set the label of the FileUpload.
<template>
<PFileUpload label="Drop your image here" class="w-96 min-h-48" />
</template>
Description
Use the description prop to set the description of the FileUpload.
<template>
<PFileUpload
label="Drop your image here"
description="SVG, PNG, JPG or GIF (max. 2MB)"
class="w-96 min-h-48"
/>
</template>
Icon
Use the icon prop to set the icon of the FileUpload. Defaults to i-lucide:upload.
<template>
<PFileUpload
icon="i-lucide:image"
label="Drop your image here"
description="SVG, PNG, JPG or GIF (max. 2MB)"
class="w-96 min-h-48"
/>
</template>
Color
Use the color prop to change the color of the FileUpload.
<template>
<PFileUpload
color="neutral"
highlight
label="Drop your image here"
description="SVG, PNG, JPG or GIF (max. 2MB)"
class="w-96 min-h-48"
/>
</template>
highlight prop is used here to show the focus state. It's used internally when a validation error occurs.Variant
Use the variant prop to change the variant of the FileUpload.
<template>
<PFileUpload variant="button" />
</template>
Size
Use the size prop to change the size of the FileUpload.
<template>
<PFileUpload
size="xl"
variant="area"
label="Drop your image here"
description="SVG, PNG, JPG or GIF (max. 2MB)"
/>
</template>
Layout
Use the layout prop to change how the files are displayed in the FileUpload. Defaults to grid.
variant is area.<template>
<PFileUpload
layout="list"
multiple
label="Drop your images here"
description="SVG, PNG, JPG or GIF (max. 2MB)"
class="w-96"
:pohon="{
base: 'min-h-48'
}"
/>
</template>
Position
Use the position prop to change the position of the files in the FileUpload. Defaults to outside.
variant is area and when layout is list.<template>
<PFileUpload
position="inside"
layout="list"
multiple
label="Drop your images here"
description="SVG, PNG, JPG or GIF (max. 2MB)"
class="w-96"
:pohon="{
base: 'min-h-48'
}"
/>
</template>
Examples
With Form validation
You can use the FileUpload within a Form and FormField components to handle validation and error handling.
<script setup lang="ts">
import type { FormSubmitEvent } from 'pohon-ui'
import { reactive } from 'vue'
import * as z from 'zod'
const MAX_FILE_SIZE = 2 * 1024 * 1024 // 2MB
const MIN_DIMENSIONS = { width: 200, height: 200 }
const MAX_DIMENSIONS = { width: 4096, height: 4096 }
const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']
function formatBytes(bytes: number, decimals = 2) {
if (bytes === 0) {
return '0 Bytes'
}
const k = 1024
const dm = decimals < 0 ? 0 : decimals
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${Number.parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`
}
const schema = z.object({
image: z
.instanceof(File, {
message: 'Please select an image file.'
})
.refine((file) => file.size <= MAX_FILE_SIZE, {
message: `The image is too large. Please choose an image smaller than ${formatBytes(MAX_FILE_SIZE)}.`
})
.refine((file) => ACCEPTED_IMAGE_TYPES.includes(file.type), {
message: 'Please upload a valid image file (JPEG, PNG, or WebP).'
})
.refine(
(file) =>
new Promise((resolve) => {
const reader = new FileReader()
reader.onload = (e) => {
const img = new Image()
img.onload = () => {
const meetsDimensions =
img.width >= MIN_DIMENSIONS.width &&
img.height >= MIN_DIMENSIONS.height &&
img.width <= MAX_DIMENSIONS.width &&
img.height <= MAX_DIMENSIONS.height
resolve(meetsDimensions)
}
img.src = e.target?.result as string
}
reader.readAsDataURL(file)
}),
{
message: `The image dimensions are invalid. Please upload an image between ${MIN_DIMENSIONS.width}x${MIN_DIMENSIONS.height} and ${MAX_DIMENSIONS.width}x${MAX_DIMENSIONS.height} pixels.`
}
)
})
type SchemaType = z.output<typeof schema>
const state = reactive<Partial<SchemaType>>({
image: undefined
})
async function onSubmit(event: FormSubmitEvent<SchemaType>) {
console.log(event.data)
}
</script>
<template>
<PForm :schema="schema" :state="state" class="w-96 space-y-4" @submit="onSubmit">
<PFormField name="image" label="Image" description="JPG, GIF or PNG. 2MB Max.">
<PFileUpload v-model="state.image" accept="image/*" class="min-h-48" />
</PFormField>
<PButton type="submit" label="Submit" color="neutral" />
</PForm>
</template>
With default slot
You can use the default slot to make your own FileUpload component.
<script setup lang="ts">
import type { FormSubmitEvent } from 'pohon-ui'
import { reactive } from 'vue'
import * as z from 'zod'
const MAX_FILE_SIZE = 2 * 1024 * 1024 // 2MB
const MIN_DIMENSIONS = { width: 200, height: 200 }
const MAX_DIMENSIONS = { width: 4096, height: 4096 }
const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']
function formatBytes(bytes: number, decimals = 2) {
if (bytes === 0) {
return '0 Bytes'
}
const k = 1024
const dm = decimals < 0 ? 0 : decimals
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${Number.parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`
}
const schema = z.object({
avatar: z
.instanceof(File, {
message: 'Please select an image file.'
})
.refine((file) => file.size <= MAX_FILE_SIZE, {
message: `The image is too large. Please choose an image smaller than ${formatBytes(MAX_FILE_SIZE)}.`
})
.refine((file) => ACCEPTED_IMAGE_TYPES.includes(file.type), {
message: 'Please upload a valid image file (JPEG, PNG, or WebP).'
})
.refine(
(file) =>
new Promise((resolve) => {
const reader = new FileReader()
reader.onload = (e) => {
const img = new Image()
img.onload = () => {
const meetsDimensions =
img.width >= MIN_DIMENSIONS.width &&
img.height >= MIN_DIMENSIONS.height &&
img.width <= MAX_DIMENSIONS.width &&
img.height <= MAX_DIMENSIONS.height
resolve(meetsDimensions)
}
img.src = e.target?.result as string
}
reader.readAsDataURL(file)
}),
{
message: `The image dimensions are invalid. Please upload an image between ${MIN_DIMENSIONS.width}x${MIN_DIMENSIONS.height} and ${MAX_DIMENSIONS.width}x${MAX_DIMENSIONS.height} pixels.`
}
)
})
type SchemaType = z.output<typeof schema>
const state = reactive<Partial<SchemaType>>({
avatar: undefined
})
function createObjectUrl(file: File): string {
return URL.createObjectURL(file)
}
async function onSubmit(event: FormSubmitEvent<SchemaType>) {
console.log(event.data)
}
</script>
<template>
<PForm :schema="schema" :state="state" class="w-64 space-y-4" @submit="onSubmit">
<PFormField name="avatar" label="Avatar" description="JPG, GIF or PNG. 1MB Max.">
<PFileUpload v-slot="{ open, removeFile }" v-model="state.avatar" accept="image/*">
<div class="flex flex-wrap gap-3 items-center">
<PAvatar
size="lg"
:src="state.avatar ? createObjectUrl(state.avatar) : undefined"
icon="i-lucide:image"
/>
<PButton
:label="state.avatar ? 'Change image' : 'Upload image'"
color="neutral"
variant="outline"
@click="open()"
/>
</div>
<p v-if="state.avatar" class="text-xs color-text-muted mt-1.5">
{{ state.avatar.name }}
<PButton
label="Remove"
color="error"
variant="link"
size="xs"
class="p-0"
@click="removeFile()"
/>
</p>
</PFileUpload>
</PFormField>
<PButton type="submit" label="Submit" color="neutral" />
</PForm>
</template>
With files-bottom slot
You can use the files-bottom slot to add a Button under the files list to remove all files for example.
<script setup lang="ts">
import { ref } from 'vue'
const value = ref<Array<File>>([])
</script>
<template>
<PFileUpload
v-model="value"
icon="i-lucide:image"
label="Drop your images here"
description="SVG, PNG, JPG or GIF (max. 2MB)"
layout="list"
multiple
:interactive="false"
class="min-h-48 w-96"
>
<template #actions="{ open }">
<PButton
label="Select images"
icon="i-lucide:upload"
color="neutral"
variant="outline"
@click="open()"
/>
</template>
<template #files-bottom="{ removeFile, files }">
<PButton
v-if="files?.length"
label="Remove all files"
color="neutral"
@click="removeFile()"
/>
</template>
</PFileUpload>
</template>
With files-top slot
You can use the files-top slot to add a Button above the files list to add new files for example.
<script setup lang="ts">
import { ref } from 'vue'
const value = ref<Array<File>>([])
</script>
<template>
<PFileUpload
v-model="value"
icon="i-lucide:image"
label="Drop your images here"
description="SVG, PNG, JPG or GIF (max. 2MB)"
layout="grid"
multiple
:interactive="false"
class="min-h-48 w-96"
>
<template #actions="{ open }">
<PButton
label="Select images"
icon="i-lucide:upload"
color="neutral"
variant="outline"
@click="open()"
/>
</template>
<template #files-top="{ open, files }">
<div v-if="files?.length" class="mb-2 flex items-center justify-between">
<p class="font-bold">Files ({{ files?.length }})</p>
<PButton
icon="i-lucide:plus"
label="Add more"
color="neutral"
variant="outline"
class="-my-2"
@click="open()"
/>
</div>
</template>
</PFileUpload>
</template>
API
Props
| Prop | Default | Type |
|---|---|---|
as | 'div' | anyThe element or component this component should render as. |
id | string | |
name | string | |
icon | appConfig.pohon.icons.upload | string | objectThe icon to display. |
label | string | |
description | string | |
color | 'primary' | "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" |
variant | 'area' | "area" | "button"The |
size | 'md' | "md" | "xs" | "sm" | "lg" | "xl" |
layout | 'grid' | "list" | "grid"The layout of how files are displayed.
Only works when |
position | 'outside' | "inside" | "outside"The position of the files.
Only works when |
highlight | boolean Highlight the ring color like a focus state. | |
accept | '*' | stringSpecifies the allowed file types for the input. Provide a comma-separated list of MIME types or file extensions (e.g., "image/png,application/pdf,.jpg"). |
multiple | false | M |
reset | false | boolean Reset the file input when the dialog is opened. |
dropzone | true | boolean Create a zone that allows the user to drop files onto it. |
interactive | true | boolean Make the dropzone interactive when the user is clicking on it. |
required | boolean | |
disabled | boolean | |
fileIcon | appConfig.pohon.icons.file | string | objectThe icon to display for the file. |
fileDelete | true | boolean | Omit<PButtonProps, PLinkPropsKeys> Configure the delete button for the file.
When |
fileDeleteIcon | appConfig.pohon.icons.close | string | objectThe icon displayed to delete a file. |
preview | true | boolean Show the file preview/list after upload. |
modelValue | null | M extends true ? File[] : File | |
pohon | { root?: ClassValue; base?: ClassValue; wrapper?: ClassValue; icon?: ClassValue; avatar?: ClassValue; label?: ClassValue; description?: ClassValue; actions?: ClassValue; files?: ClassValue; file?: ClassValue; fileLeadingAvatar?: ClassValue; fileWrapper?: ClassValue; fileName?: ClassValue; fileSize?: ClassValue; fileTrailingButton?: ClassValue; } |
Slots
| Slot | Type |
|---|---|
default | { open: (localOptions?: Partial<UseFileDialogOptions> | undefined) => void; removeFile: (index?: number | undefined) => void; pohon: object; } |
leading | { pohon: object; } |
label | object |
description | object |
actions | { files?: FileUploadFiles<M> | undefined; open: (localOptions?: Partial<UseFileDialogOptions> | undefined) => void; removeFile: (index?: number | undefined) => void; } |
files | { files?: FileUploadFiles<M> | undefined; } |
files-top | { files?: FileUploadFiles<M> | undefined; open: (localOptions?: Partial<UseFileDialogOptions> | undefined) => void; removeFile: (index?: number | undefined) => void; } |
files-bottom | { files?: FileUploadFiles<M> | undefined; open: (localOptions?: Partial<UseFileDialogOptions> | undefined) => void; removeFile: (index?: number | undefined) => void; } |
file | { file: File; index: number; } |
file-leading | { file: File; index: number; pohon: object; } |
file-name | { file: File; index: number; } |
file-size | { file: File; index: number; } |
file-trailing | { file: File; index: number; pohon: object; } |
Emits
| Event | Type |
|---|---|
change | [event: Event] |
update:modelValue | [value: (M extends true ? File[] : File) | null | undefined] |
Expose
When accessing the component via a template ref, you can use the following:
| Name | Type |
|---|---|
inputRef | Ref<HTMLInputElement | null> |
dropzoneRef | Ref<HTMLDivElement | null> |
Theme
Below is the theme configuration skeleton for the PFileUpload. Since the component is provided unstyled by default, you will need to fill in these values to apply your own custom look and feel. If you prefer to use our pre-built, opinionated styling, you can instead use our UnoCSS preset, this docs is using it as well.
export default defineAppConfig({
pohon: {
fileUpload: {
slots: {
root: '',
base: '',
wrapper: '',
icon: '',
avatar: '',
label: '',
description: '',
actions: '',
files: '',
file: '',
fileLeadingAvatar: '',
fileWrapper: '',
fileName: '',
fileSize: '',
fileTrailingButton: ''
},
variants: {
color: {
primary: '',
secondary: '',
success: '',
info: '',
warning: '',
error: '',
neutral: ''
},
variant: {
area: {
wrapper: '',
base: ''
},
button: {}
},
size: {
xs: {
base: '',
icon: '',
file: '',
fileWrapper: ''
},
sm: {
base: '',
icon: '',
file: '',
fileWrapper: ''
},
md: {
base: '',
icon: '',
file: ''
},
lg: {
base: '',
icon: '',
file: '',
fileSize: ''
},
xl: {
base: '',
icon: '',
file: ''
}
},
layout: {
list: {
root: '',
files: '',
file: '',
fileTrailingButton: ''
},
grid: {
fileWrapper: '',
fileLeadingAvatar: '',
fileTrailingButton: ''
}
},
position: {
inside: '',
outside: ''
},
dropzone: {
true: ''
},
interactive: {
true: ''
},
highlight: {
true: ''
},
multiple: {
true: ''
},
disabled: {
true: ''
}
},
compoundVariants: [],
defaultVariants: {
color: 'primary',
variant: 'area',
size: 'md'
}
}
}
};
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import pohon from 'pohon-ui/vite'
export default defineAppConfig({
pohon: {
fileUpload: {
slots: {
root: '',
base: '',
wrapper: '',
icon: '',
avatar: '',
label: '',
description: '',
actions: '',
files: '',
file: '',
fileLeadingAvatar: '',
fileWrapper: '',
fileName: '',
fileSize: '',
fileTrailingButton: ''
},
variants: {
color: {
primary: '',
secondary: '',
success: '',
info: '',
warning: '',
error: '',
neutral: ''
},
variant: {
area: {
wrapper: '',
base: ''
},
button: {}
},
size: {
xs: {
base: '',
icon: '',
file: '',
fileWrapper: ''
},
sm: {
base: '',
icon: '',
file: '',
fileWrapper: ''
},
md: {
base: '',
icon: '',
file: ''
},
lg: {
base: '',
icon: '',
file: '',
fileSize: ''
},
xl: {
base: '',
icon: '',
file: ''
}
},
layout: {
list: {
root: '',
files: '',
file: '',
fileTrailingButton: ''
},
grid: {
fileWrapper: '',
fileLeadingAvatar: '',
fileTrailingButton: ''
}
},
position: {
inside: '',
outside: ''
},
dropzone: {
true: ''
},
interactive: {
true: ''
},
highlight: {
true: ''
},
multiple: {
true: ''
},
disabled: {
true: ''
}
},
compoundVariants: [],
defaultVariants: {
color: 'primary',
variant: 'area',
size: 'md'
}
}
}
};