Skip to main content

FileUpload

A file upload is a dropzone with a hidden <input type="file"> behind a Button. The button is the accessible control; the whole dropzone is also a click target and accepts drops. Selected files are listed under it with remove buttons, and a polite live region announces the count after every change.

import FileUpload from '@zuilib/components/file-upload'

Basic

Single mode replaces the file on every pick. The description under the trigger is built from accept and maxSize unless you pass one.

Loading example

Multiple with limits

multiple appends to the list (duplicates by name, size and mtime are skipped). Files failing accept, maxSize or maxFiles are dropped from the selection, listed in an error under the dropzone, and passed to onError; the dropzone turns invalid until the next successful change.

Loading example

Sizes and text

sm, md and lg scale the trigger button (the shared control sizes), the padding, the icon and the list text. buttonText, dropText, description and icon replace the built-in copy; icon={null} hides the glyph and showList={false} hides the list.

Loading example

In a FormItem

Inside a FormItem the trigger takes the field's control id, so the Label focuses it, and the item's Description / Message join the component's own description and error in the trigger's aria-describedby. disabled and invalid inherit from the item; name puts the shown list on the native input so a native form submit sends it.

Loading example

Props

FileUploadProps extends HTMLAttributes<HTMLDivElement> (minus onChange, defaultValue, onError) with:

PropTypeDefaultDescription
valueFile[]Controlled list. A reset from the parent also clears rejection errors
defaultValueFile[][]Uncontrolled initial list
onChange(files: File[]) => voidThe full list after every add / remove; rejected files are left out
onError(errors: FileUploadError[]) => voidFiles rejected by accept / maxSize / maxFiles. Each error is { code, file, message } with code 'accept' | 'size' | 'count'
acceptstringNative syntax ("image/*,.pdf"); dropped files are checked against it too
multiplebooleanfalseAppend (deduplicated) instead of replace
maxSizenumberPer-file limit in bytes
maxFilesnumberTotal count (multiple only)
disabledbooleanFormItem disabled
invalidbooleanFormItem invalidaria-invalid on the trigger, data-invalid on the dropzone. A rejected selection sets it too
namestringName of the native input; its file list mirrors the shown list
idstringFormItem control idGoes on the trigger button (what a Label htmlFor focuses)
buttonTextReactNode'Choose file' / 'Choose files'
dropTextReactNode'or drag and drop'Text beside the button
descriptionReactNodefrom accept / maxSizeHelper line under the trigger
iconReactNodeupload iconPass null to hide
showListbooleantrue
formatSize(bytes: number) => stringformatFileSize1536 to 1.5 KB; the default is a named export
formatError({ code, file }) => stringEnglish sentencesText of a rejection, shown under the dropzone and passed to onError
statusText(files: File[]) => ReactNode'N files selected'Content of the polite live region
removeLabel(file: File) => string'Remove <name>'Accessible label of each remove button
size'sm' | 'md' | 'lg''md'
classNamestringMerged last onto the root
dropzoneClassNamestringMerged last onto the dropzone

formatFileSize and the FileUploadError / FileUploadErrorCode types are named exports of the same module.

Slots

SlotElementNotes
[data-slot="file-upload"]divRoot. data-size, data-state="default|error", data-invalid, data-disabled
[data-slot="file-upload-dropzone"]divClick and drop target. data-state, data-dragging, data-invalid, data-disabled
[data-slot="file-upload-input"]inputThe hidden native file input
[data-slot="file-upload-icon"]svgThe default upload glyph
[data-slot="file-upload-actions"]divWraps the trigger and drop text
[data-slot="file-upload-trigger"]buttonThe accessible control (an outline Button)
[data-slot="file-upload-drop-text"]span
[data-slot="file-upload-description"]p
[data-slot="file-upload-error"]pA Message with role="alert", present while a rejection stands
[data-slot="file-upload-status"]spanVisually hidden polite live region
[data-slot="file-upload-list"]ul
[data-slot="file-upload-item"]liParts: file-upload-item-icon, file-upload-item-name, file-upload-item-size, file-upload-item-remove, file-upload-item-remove-icon
/* Consumer CSS: solid dropzone border while dragging */
[data-slot="file-upload-dropzone"][data-dragging] {
border-style: solid;
}

Tokens

TokenUsed for
--control-height-sm/md/lgTrigger height; remove buttons are --control-height-sm squares
--input, --background, --accentDropzone border, surface and hover / dragging fill
--primaryDropzone border while dragging
--destructiveInvalid border and the error text
--card, --card-foreground, --borderFile list items
--muted-foregroundIcons, drop text, description, sizes
--radius-lg, --radius-mdDropzone and item corners
--duration-fastColour transition

Accessibility

  • The trigger is a real <button>: keyboard users press Enter or Space to open the picker. The native input is aria-hidden and out of the tab order.
  • Inside a FormItem the trigger takes the Field's control id and aria-labelledby (the item's Label, then the button's own text, so the name reads "Attachments Choose files"); the item's Description / Message, the component's description and any error join aria-describedby. aria-invalid follows the item.
  • Rejections render in a role="alert" message; the file count is announced in a polite role="status" region after every add or remove.
  • Each remove button is labelled Remove <name>; removing a file moves focus to the next item's remove button, else the previous one, else the trigger, so focus never lands on nothing.
  • disabled blocks picks, drops and removal, sets data-disabled on the root and dropzone and disables the buttons.
  • Drag and drop is an enhancement; every file can be added through the button.
  • FormItem: label, description and message wiring.
  • Button: the trigger and remove buttons.
  • Message: the rejection text.