Skip to content
Design System
Contact

File upload

Drop zone with type and size validation, progress and removal. Files stay in the browser.

ComponentsDrag and drop API

Playground

Drop real files or use the sample batch. Validation, progress, failure and retry all run locally: only the name, size and type are read, and nothing is uploaded.

Preview

Files stay in your browser. Nothing is sent to a server.

Props

Variant
Max size
example.tsxtsx
1import { FileUpload } from "@/components/ui/file-upload"
2
3<FileUpload
4 accept="image/png,image/jpeg,application/pdf"
5 maxSize={5 * 1024 * 1024}
6 onUpload={(file, { onProgress, signal }) => uploadToStorage(file, { onProgress, signal })}
7/>

States

The drop target at rest, hovered, focused and disabled, then a file row while uploading, after a rejection and when complete.

States
  • Defaultrest
  • Hover:hover
  • Focus:focus-visible
    • acme-msa-2026.pdf

      48%

      885 KB of 1.8 MB

    Loadingrole=progressbar
  • Disableddisabled
    • floorplan-scan.jpg

      Not added

      7.2 MB

      This file is 7.2 MB; the limit is 5 MB. Compress it or export at a lower resolution.

    Errordata-status=rejected
    • acme-msa-2026.pdf

      Uploaded

      1.8 MB

    Successdata-status=complete

Examples

The sample batch walks every path: two clean uploads, a dropped connection you can retry, a file of the wrong type and one over the size limit.

Attach documents to a deal
Demo data

Acme Corporation renewal

Contracts and invoices for the 2026 renewal

Files stay in your browser. Nothing is sent to a server.

Drag-over state
Compact, in a comment composer
PDF, DOCX or CSV up to 10 MB each, or drop them here

Files stay in your browser. Nothing is sent to a server.

Avatar, in profile settings

Profile photo

PNG or JPG up to 5 MB. Drop an image on the circle or choose one.

Files stay in your browser. Nothing is sent to a server.

API

PropTypeDefaultDescription
variant"dropzone" | "compact" | "avatar""dropzone"Large drop target, an inline attach button, or a single-image avatar with a progress ring.
acceptstring"image/png,image/jpeg,application/pdf"Same syntax as <input accept>. Checked again on drop, because drag and drop ignores the attribute.
maxSizenumber5 * 1024 * 1024Per-file limit in bytes. Larger files are listed with an error that says how to fix them.
multiplebooleantrueAllow several files. Ignored by the avatar variant, which replaces its single file.
disabledbooleanfalseDisables the button and file input and refuses drops.
onUpload(file: File, ctx: { onProgress(pct: number): void; signal: AbortSignal }) => Promise<void>—Transport for accepted files. Cancel aborts the signal. These docs use a simulated transport.
onChange(items: UploadItem[]) => void—Called when the queue changes, with each item's status: uploading, complete, error or rejected.

Accessibility

Keyboard

Focus the drop zone, then each file's actions
Tab
Open the file dialog
EnterorSpace
Cancel, retry or remove the focused file
Enter

Semantics and ARIA

  • The drop zone is a real <button> that opens a hidden <input type="file">, so drag and drop is never the only way in.
  • Each upload renders role="progressbar" with aria-valuenow and a label naming the file.
  • A polite role="status" region announces transitions (uploading, uploaded, failed) instead of every percent.
  • Errors are text next to the file and linked with aria-describedby, and say how to fix the problem, not just what went wrong.
  • Icon buttons carry the file name in their label, for example Retry globex-invoice-0923.pdf.
  • Files are never read or sent: validation uses name, size and type only.