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>
Use the multiple prop to allow multiple files to be selected.
<template>
<PFileUpload multiple class="w-96 min-h-48" />
</template>
Use the dropzone prop to enable/disable the droppable area. Defaults to true.
<template>
<PFileUpload class="w-96 min-h-48" />
</template>
Use the interactive prop to enable/disable the clickable area. Defaults to true.
<template>
<PFileUpload class="w-96 min-h-48" />
</template>
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>
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>
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>
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>
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.Use the variant prop to change the variant of the FileUpload.
<template>
<PFileUpload variant="button" />
</template>
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>
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>
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>
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>
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>
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>
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>
| Prop | Default | Type |
|---|
| Slot | Type |
|---|
| Event | Type |
|---|
When accessing the component via a template ref, you can use the following:
| Name | Type |
|---|---|
inputRef | Ref<HTMLInputElement | null> |
dropzoneRef | Ref<HTMLDivElement | null> |
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'
}
}
}
};