Skip to content

File upload

Dropzone with accept/size/count validation, a hidden file input, and a removable file list.

Primitive · form

Terminal window
npx shadcn@latest add https://ndigo.dev/r/file-upload.json

To use the shorter @ndigo/file-upload form, configure the namespace in components.json.

Source Target Type
registry/ui/file-upload/nd-file-upload.ts @ui/nd-file-upload/nd-file-upload.ts registry:ui
registry/ui/file-upload/nd-file-upload.css @ui/nd-file-upload/nd-file-upload.css registry:file

See this component rendered live on the live examples page.

Registry: Ndigo element foundation. Packages: @zag-js/file-upload@~1.47.0 (installed automatically).

<nd-file-upload> opens the browser picker from the dropzone or trigger button, accepts drops, validates files, and lists them with remove and clear actions. The hidden file input carries the form data.

<nd-file-upload name="attachments" accept="image/*,.pdf" max-files="3">
<label data-part="label">Attachments</label>
<div data-part="dropzone">Drop files here or use the button below.</div>
<button data-part="trigger" type="button">Choose files</button>
<button data-part="clear-trigger" type="button">Clear</button>
</nd-file-upload>

The element is the machine root and creates the hidden file input and the item-group list. The dropzone is the clickable/drop surface; keep the trigger separate so the dropzone does not nest a button inside its role="button".

Attribute Type Description
name string Form field name for the file input
accept string Comma-separated MIME types/extensions
max-files number Maximum number of files (default 1)
max-file-size number Maximum size per file in bytes
min-file-size number Minimum size per file in bytes
disabled boolean Disable the control
readonly boolean Keep the value but block changes
required boolean Mark the file input required
invalid boolean Mark the control invalid

max-files above 1 sets multiple on the input.

  • nd-file-change — detail: { acceptedFiles: File[], rejectedFiles: { file, errors }[] }. data-value lists accepted file names joined with ", ", and data-count holds the accepted count.
  • openFilePicker() — open the browser picker.
  • clear() — remove every accepted file.

The dropzone is a labelled role="button" and responds to Enter/Space; the trigger is a native button. The hidden file input is taken out of the tab order and the accessibility tree because the dropzone/trigger provide the interface. Each item’s remove button is labelled per file. Rejected files are reported through the event rather than rendered, so the accepted list stays unambiguous.

Import nd-file-upload.css after the tokens. The dropzone is a dashed surface that switches to the accent tokens on hover, focus, and drag; items are compact surface rows with a muted size label.