FileUpload

GitHub
An input element to upload files.

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 class="w-96 min-h-48" />
</template>

Interactive

Use the interactive prop to enable/disable the clickable area. Defaults to true.

This can be useful when adding a Button component in the #actions slot.
<template>
  <PFileUpload 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.

Drop your image here
<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.

Drop your image here
SVG, PNG, JPG or GIF (max. 2MB)
<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.

Drop your image here
SVG, PNG, JPG or GIF (max. 2MB)
<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>
You can customize this icon globally in your app.config.ts under pohon.icons.upload key.
You can customize this icon globally in your vite.config.ts under pohon.icons.upload key.

Color

Use the color prop to change the color of the FileUpload.

Drop your image here
SVG, PNG, JPG or GIF (max. 2MB)
<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>
The 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.

Drop your image here
SVG, PNG, JPG or GIF (max. 2MB)
<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.

This prop only works when variant is area.
Drop your images here
SVG, PNG, JPG or GIF (max. 2MB)
<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.

This prop only works when variant is area and when layout is list.
Drop your images here
SVG, PNG, JPG or GIF (max. 2MB)
<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.

JPG, GIF or PNG. 2MB Max.

<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.

JPG, GIF or PNG. 1MB Max.

<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.

Drop your images here
SVG, PNG, JPG or GIF (max. 2MB)
<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>
The interactive prop is set to false in this example to prevent the default clickable area.

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.

Drop your images here
SVG, PNG, JPG or GIF (max. 2MB)
<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
This component also supports all native <input> HTML attributes.

Slots

Slot Type

Emits

Event Type

Expose

When accessing the component via a template ref, you can use the following:

NameType
inputRefRef<HTMLInputElement | null>
dropzoneRefRef<HTMLDivElement | null>

Theme

We use unocss-variants to customize the theme. Read more about it in the theming guide.

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.

app.config.ts
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'
      }
    }
  }
};
vite.config.ts
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'
      }
    }
  }
};

Changelog

No recent changes