Skip to content

useFileUpload ​

Selecting files, checking them, and uploading each one with its own progress.

The example below adds and validates files but does not call upload() — this site has no upload endpoint, and a button that always failed would teach the wrong thing. Everything up to the POST is real.

Open in demo

Import ​

ts
import { useFileUpload } from '@shared/composables/useFileUpload'

Signature ​

ts
function useFileUpload(config: FileUploadConfig): {
  files: Ref<UploadFile[]>
  uploading: Ref<boolean>
  validate: (file: File) => string | null
  add: (incoming: File[] | FileList) => void
  remove: (target: UploadFile) => void
  upload: () => Promise<void>
}

FileUploadConfig ​

KeyTypeDescription
endpointstringWhere each file is POSTed. Required.
maxSizenumberMaximum bytes per file.
acceptedTypesstring[]MIME (image/png), wildcard (image/*) or extension (.pdf).
multiplebooleanfalse keeps one file, replacing what was there.
maxFilesnumberThe cap. Extra files are ignored, not rejected.

Returns ​

MemberTypeDescription
filesRef<UploadFile[]>Everything added, valid or not.
uploadingRef<boolean>An upload round is in flight.
validate(file) => string | nullThe error message, or null when the file passes.
add(File[] | FileList) => voidValidates as it adds.
remove(target) => voidDrops one entry.
upload() => Promise<void>Uploads every pending file, in parallel.

UploadFile ​

KeyTypeDescription
idstringA UUID.
fileFileThe original.
progressnumber0 to 100.
status'pending' | 'uploading' | 'success' | 'error'Where it is.
errorstringWhy it failed validation or upload.

Example ​

vue
<script setup lang="ts">
import { useFileUpload } from '@shared/composables/useFileUpload'

const { files, uploading, add, remove, upload } = useFileUpload({
  endpoint: '/uploads',
  maxSize: 5 * 1024 * 1024,
  acceptedTypes: ['image/*', '.pdf'],
  multiple: true,
  maxFiles: 5,
})

function onPick(event: Event) {
  const input = event.target as HTMLInputElement
  if (input.files)
    add(input.files)
}
</script>

<template>
  <div class="space-y-3">
    <input type="file" multiple @change="onPick">

    <ul>
      <li v-for="item in files" :key="item.id">
        {{ item.file.name }} — {{ item.status }} {{ item.progress }}%
        <span v-if="item.error">{{ item.error }}</span>
        <button type="button" @click="remove(item)">
          Remove
        </button>
      </li>
    </ul>

    <button type="button" :disabled="uploading" @click="upload()">
      Upload
    </button>
  </div>
</template>

Notes ​

  • Validation runs on add, not on upload. A file that fails lands in the list with status: 'error' and its reason, so the reader sees what is wrong before pressing anything.
  • acceptedTypes matches three ways: a wildcard checks the MIME prefix, a leading dot checks the file name's ending, and anything else is an exact MIME match.
  • maxFiles stops adding; it does not reject. Once the list is full the remaining files in a drop are silently ignored.
  • With multiple: false the list is cleared first, so picking a second file replaces the first rather than sitting beside it.
  • upload() only sends pending files, so pressing it twice does not re-upload what already succeeded, and a file that failed validation is never sent.
  • Each file is its own multipart POST under the field name file, with its own progress handler. They run in parallel through Promise.all.
  • A failed upload sets that file's status to error and leaves the rest alone. The promise still resolves.
  • The whole feature sits behind config.features.fileUpload.
  • File upload — the drop zone built on this.
  • Schema — the file field type and its upload options.