Skip to content
Nizivo UI

FileUpload

A dropzone over a native file input, with a list of the chosen files. Before any JavaScript runs, a click or a drop chooses files and a plain form submits them; hydrated, it checks each file against accept and maxSize and shows the upload state your code reports for each one.

Tier: own

Import

import { FileUpload } from '@nizivo/react/file-upload';

// Once per app (Getting started), after
// @nizivo/tokens/tokens.css and @nizivo/react/css/base.css and @nizivo/react/css/button.css and @nizivo/react/css/icon-button.css and @nizivo/react/css/inline-error.css and @nizivo/react/css/progress-bar.css
import '@nizivo/react/css/file-upload.css';

Static or interactive

Client. FileUpload is a client component that reads its Field through FieldScope, so a page with one also loads FieldScope and React Aria's drop hook. Its server HTML is complete and works before hydration: a native <input type="file"> covers the dropzone, so a click opens the picker and a file dropped on it is chosen natively, and a form submits the chosen files under name. How to choose.

In a client app (a Vite or React Router single-page app, or a 'use client' file) it is an ordinary React component: event handlers, refs and state work as usual. In a client app.

The form submits the files

FileUpload never uploads anything. The native input holds the chosen files, and the component writes the list back into it after every pick, drop and remove, so a form (a Server Action, or method="post" with encType="multipart/form-data") submits exactly the files you see. With multiple, each pick or drop adds to the list; without it, a new file replaces the old one.

A file that accept does not match, or that is larger than maxSize (in bytes; 1 MB is 1,000,000), is not added: its message appears under the dropzone, is read by screen readers and describes the input. onReject receives the refused files. Check the files on the server too: before hydration, the browser submits whatever was chosen.

Inside a Field, the Field's label names the input, its description and errors describe it, and required is native: a submit with no file shows the Field's valueMissing message, and choosing a file withdraws it. A click on the Field's label focuses the dropzone without opening the picker.

Uploading as files are chosen

When your code sends each file as soon as it is chosen, onFilesChange gives every held file with an id the component assigned; key your requests by it, and pass each file's state back as items:

'use client';
const [items, setItems] = useState<FileUploadItem[]>([]);

<FileUpload
  multiple
  items={items}
  onFilesChange={(files) => {
    for (const { id, file } of files) startUpload(id, file, setItems);
  }}
  onRetry={(id) => retryUpload(id)}
  onCancel={(id) => abortUpload(id)}
  onRemove={(id) => deleteUpload(id)}
/>;

An item is { id, status, progress, error }, with status uploading (a ProgressBar, determinate when you pass progress, 0 to 100), complete or error (with a Retry button). While any item is uploading, the dropzone's line shows how many are done. Cancel and Remove take a held file out of the list and the input before they call you. An item whose id matches no held file is listed too, after the held ones, so an edit form can show files uploaded earlier; give it a name.

Leave name off the FileUpload when you upload this way, or the form submits the files a second time; submit your upload ids in your own hidden inputs.

Strings

Every string is in labels, with English defaults: prompt ({browse} marks where the brand coloured word goes), browse, release (shown while files are dragged over), summary, the rejection messages wrongType, tooLarge ({name}, {max}) and tooMany, and the list's remove, retry, cancel, uploading, complete and failed. Sizes are formatted for locale. Pass hint for the line under the prompt and icon for the mark above it.

Examples

Basic

Inside a Field, in a form that submits the files itself: a click or a drop on the zone chooses them, before hydration too, and the chosen files submit under name.

Drop product images or browsePNG, JPG or WebP, up to 20 MB
import type { JSX } from 'react';
import { Field } from '@nizivo/react/field';
import { FileUpload } from '@nizivo/react/file-upload';

/** Stands in for an `@nizivo/icons` glyph (Lucide's `upload`). */
function UploadMark(): JSX.Element {
  return (
    <svg
      viewBox="0 0 24 24"
      width="1em"
      height="1em"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.92}
      strokeLinecap="round"
      strokeLinejoin="round"
    >
      <path d="M12 3v12" />
      <path d="m17 8-5-5-5 5" />
      <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
    </svg>
  );
}

export function FileUploadExample(): JSX.Element {
  return (
    <div style={{ maxInlineSize: 400 }}>
      <Field
        label="Product images"
        required
        messages={{ valueMissing: 'Add at least one product image.' }}
      >
        <FileUpload
          name="images"
          accept="image/png,image/jpeg,image/webp"
          multiple
          maxSize={20_000_000}
          icon={<UploadMark />}
          hint="PNG, JPG or WebP, up to 20 MB"
          labels={{ prompt: 'Drop product images or {browse}' }}
        />
      </Field>
    </div>
  );
}

States

The upload state your code passes back as items, one per file: uploading with its progress, complete, and failed with a retry. These items match no chosen file, as files uploaded earlier.

1 of 3 uploadedPDF or images, up to 10 MB
  • packing-slip.pdf248 kB
  • invoice-1042.pdf1.2 MBUploaded
  • damaged-box.jpg3.4 MBThe connection dropped
import type { JSX } from 'react';
import { FileUpload } from '@nizivo/react/file-upload';

/** Stands in for an `@nizivo/icons` glyph (Lucide's `upload`). */
function UploadMark(): JSX.Element {
  return (
    <svg
      viewBox="0 0 24 24"
      width="1em"
      height="1em"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.92}
      strokeLinecap="round"
      strokeLinejoin="round"
    >
      <path d="M12 3v12" />
      <path d="m17 8-5-5-5 5" />
      <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
    </svg>
  );
}

export function FileUploadStatesExample(): JSX.Element {
  return (
    <div style={{ maxInlineSize: 400 }}>
      <FileUpload
        aria-label="Order attachments"
        multiple
        icon={<UploadMark />}
        hint="PDF or images, up to 10 MB"
        items={[
          { id: 'a', name: 'packing-slip.pdf', size: 248_000, status: 'uploading', progress: 60 },
          { id: 'b', name: 'invoice-1042.pdf', size: 1_200_000, status: 'complete' },
          {
            id: 'c',
            name: 'damaged-box.jpg',
            size: 3_400_000,
            status: 'error',
            error: 'The connection dropped',
          },
        ]}
      />
    </div>
  );
}

Disabled

Disabled: the zone takes no pick and no drop.

Drop files here or browsePDF, up to 5 MB
Unlocks once the order is paid.
import type { JSX } from 'react';
import { Field } from '@nizivo/react/field';
import { FileUpload } from '@nizivo/react/file-upload';

/** Stands in for an `@nizivo/icons` glyph (Lucide's `upload`). */
function UploadMark(): JSX.Element {
  return (
    <svg
      viewBox="0 0 24 24"
      width="1em"
      height="1em"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.92}
      strokeLinecap="round"
      strokeLinejoin="round"
    >
      <path d="M12 3v12" />
      <path d="m17 8-5-5-5 5" />
      <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
    </svg>
  );
}

export function FileUploadDisabledExample(): JSX.Element {
  return (
    <div style={{ maxInlineSize: 400 }}>
      <Field label="Invoice" disabled description="Unlocks once the order is paid.">
        <FileUpload name="invoice" accept=".pdf" icon={<UploadMark />} hint="PDF, up to 5 MB" />
      </Field>
    </div>
  );
}

Props

className and style go on the root; every other input prop, ref included, goes on the native <input type="file">.

Client component Renders data-slot="file-upload" with the class nz-file-upload. Props: FileUploadProps. Source: src/file-upload/file-upload.client.tsx.

NameTypeDefaultDescription
acceptstring–

The native accept: MIME types (image/png), wildcards (image/*) and extensions (.pdf), comma-separated. The picker filters by it, and a dropped or picked file that does not match is not added.

hintReactNode–

The line under the prompt, such as "PNG, JPG or WebP, up to 20 MB".

iconReactNode–

Decorative mark above the prompt, such as your icon set's upload glyph (aria-hidden).

invalidboolean–

An error message is presented for this control (§8.3). Inside a Field, the Field sets it; an explicit value here wins.

itemsreadonly FileUploadItem[]–

Upload state per file, keyed by the ids onFilesChange gives. Leave it out when the form submits the files itself.

labelsPartial<FileUploadLabels>–
localestring'en-US'

BCP 47 tag for sizes.

maxSizenumber–

Bytes. A larger file is not added. 1 MB is 1,000,000 bytes, as {max} is written.

multiplebooleanfalse

More than one file; each pick or drop adds to the list. Without it, a new file replaces the old.

onCancel(id: string) => void–

An item's cancel button, on an item uploading. The held file has already left the list.

onFilesChange(files: FileUploadFile[]) => void–

The held files changed: a pick, a drop, a remove, a cancel or the form's reset. Every held file, in order.

onReject(rejections: FileUploadRejection[]) => void–

Files of a pick or drop that were not added; their messages show under the dropzone.

onRemove(id: string) => void–

An item's remove button. A held file has already left the list and the input.

onRetry(id: string) => void–

An item's retry button, on an item in error.

  • Also takes every attribute of <input> except accept, children, defaultValue, multiple, onChange, size, type, value. (Omit<ComponentPropsWithRef<"input">, "type" | "size" | "children" | "value" | "defaultValue" | "onChange" | "accept" | "multiple">)

Other exports

FileUploadFile type

import type { FileUploadFile } from '@nizivo/react/file-upload'

A file the component holds (and its input submits), with the id the component gave it.

NameTypeDefaultDescription
filerequiredFile–
idrequiredstring–

Stays the same while the file is listed. Key your items and upload requests by it.

FileUploadItem type

import type { FileUploadItem } from '@nizivo/react/file-upload'

An item's upload state, from the app, which does the uploading. Matched to a held file by id; an item whose id matches no held file is listed too, after the held files (a file uploaded earlier, shown on an edit form).

NameTypeDefaultDescription
errorstringlabels.failed

The message while status is error.

idrequiredstring–
namestring–

Shown in the list. Defaults to the held file's name; an item that matches no held file needs one, or its id is shown.

progressnumber–

0 to 100 while uploading; absent, the bar is indeterminate.

sizenumber–

In bytes, shown beside the name. Defaults to the held file's size.

statusFileUploadStatus
"uploading""complete""error"
–
FileUploadLabels type

import type { FileUploadLabels } from '@nizivo/react/file-upload'

Strings FileUpload renders (§16.4): serialisable, English defaults. {name} is a file's name, {max} the formatted maxSize, {browse} where the browse word goes, {complete} and {total} counts.

NameTypeDefaultDescription
browserequiredstring'browse'

The word that reads as the picker's button, in the brand colour.

cancelrequiredstring'Cancel'

An item uploading.

completerequiredstring'Uploaded'

A complete item's status.

failedrequiredstring'Upload failed'

An item in error with no error text.

listrequiredstring'Chosen files'

The list's accessible name.

promptrequiredstring'Drop files here or {browse}'

The dropzone's line.

releaserequiredstring'Release to add files'

Replaces the prompt while a drag with files is over the dropzone.

removerequiredstring'Remove {name}'

Each item's remove button.

retryrequiredstring'Retry'

An item in error.

summaryrequiredstring'{complete} of {total} uploaded'

Replaces the prompt while an item is uploading.

tooLargerequiredstring'{name} is larger than {max}.'

A file over maxSize.

tooManyrequiredstring'{name} was not added: choose one file.'

Without multiple, every file of a drop after the first.

uploadingrequiredstring'Uploading {name}'

An uploading item's progress bar.

wrongTyperequiredstring'{name} is not an accepted file type.'

A file accept does not match.

FileUploadProps type

import type { FileUploadProps } from '@nizivo/react/file-upload'

The props of FileUpload, listed above.

FileUploadRejection type

import type { FileUploadRejection } from '@nizivo/react/file-upload'

NameTypeDefaultDescription
filerequiredFile–
reasonrequiredFileUploadRejectionReason
"type""size""count"
–
FileUploadRejectionReason type

import type { FileUploadRejectionReason } from '@nizivo/react/file-upload'

Why a chosen or dropped file was not added.

type FileUploadRejectionReason = "type" | "size" | "count"

FileUploadStatus type

import type { FileUploadStatus } from '@nizivo/react/file-upload'

Where an item's upload is. Absent on an item: chosen, not sent yet.

type FileUploadStatus = "uploading" | "complete" | "error"

Data attributes and slots

Style parts by class (.nz-<slot>) and state by data attribute. Attribute meanings come from contracts/vocabulary.json.

SlotRendered byData attributes
file-upload
.nz-file-upload
FileUpload
root (no render)
none
file-upload-browse
.nz-file-upload-browse
FileUpload
inner element (CSS only)
none
file-upload-dropzone
.nz-file-upload-dropzone
FileUpload
inner element (CSS only); interactive, focus: nested-input; forced colours: outline on [data-drag-over]
  • data-drag-over (present or absent)
    A drag carrying files is over the dropzone: dropping adds them
file-upload-hint
.nz-file-upload-hint
FileUpload
inner element (CSS only)
none
file-upload-icon
.nz-file-upload-icon
FileUpload
inner element (CSS only)
none
file-upload-input
.nz-file-upload-input
FileUpload
inner element (CSS only)
none
file-upload-item
.nz-file-upload-item
FileUpload
inner element (CSS only)
none
file-upload-item-actions
.nz-file-upload-item-actions
FileUpload
inner element (CSS only)
none
file-upload-item-complete
.nz-file-upload-item-complete
FileUpload
inner element (CSS only)
none
file-upload-item-error
.nz-file-upload-item-error
FileUpload
inner element (CSS only)
none
file-upload-item-name
.nz-file-upload-item-name
FileUpload
inner element (CSS only)
none
file-upload-item-size
.nz-file-upload-item-size
FileUpload
inner element (CSS only)
none
file-upload-item-status
.nz-file-upload-item-status
FileUpload
inner element (CSS only)
none
file-upload-list
.nz-file-upload-list
FileUpload
inner element (CSS only)
none
file-upload-messages
.nz-file-upload-messages
FileUpload
inner element (CSS only)
none
file-upload-prompt
.nz-file-upload-prompt
FileUpload
inner element (CSS only)
none

CSS hooks

Set a hook on the component's class or any ancestor to restyle it without specificity games; leave it unset for the default shown.

HookDefaultRead as
--nz-file-upload-bg
  • default: var(--nz-bg-subtle)
  • data-disabled: var(--nz-bg-disabled)
  • data-drag-over: var(--nz-bg-brand-subtle)
  • background on .nz-file-upload-dropzone
--nz-file-upload-border
  • default: var(--nz-border-strong)
  • default @media (hover: hover): var(--nz-border-control-hover)
  • data-disabled: var(--nz-border-subtle)
  • data-drag-over: var(--nz-border-brand)
  • data-invalid: var(--nz-border-critical)
  • border on .nz-file-upload-dropzone
--nz-file-upload-browse-fg--nz-text-brand
  • color on .nz-file-upload-browse
--nz-file-upload-gap--nz-space-3
  • gap on .nz-file-upload
--nz-file-upload-hint-fg--nz-text-secondary
  • color on .nz-file-upload-hint
--nz-file-upload-icon-bg
  • default: var(--nz-bg-surface)
  • data-drag-over: var(--nz-bg-brand)
  • background on .nz-file-upload-icon
--nz-file-upload-icon-fg
  • default: var(--nz-icon-primary)
  • data-disabled: var(--nz-text-disabled)
  • data-drag-over: var(--nz-text-on-brand)
  • color on .nz-file-upload-icon
--nz-file-upload-item-bg--nz-bg-surface
  • background on .nz-file-upload-item
--nz-file-upload-item-radius--nz-radius-md
  • border-radius on .nz-file-upload-item
--nz-file-upload-padding-block--nz-space-6
  • padding-block on .nz-file-upload-dropzone
--nz-file-upload-padding-inline--nz-space-6
  • padding-inline on .nz-file-upload-dropzone
--nz-file-upload-radius--nz-radius-xl
  • border-radius on .nz-file-upload-dropzone

Layers: nz.components. Classes: .nz-file-upload, .nz-file-upload-browse, .nz-file-upload-dropzone, .nz-file-upload-hint, .nz-file-upload-icon, .nz-file-upload-input, .nz-file-upload-item, .nz-file-upload-item-actions, .nz-file-upload-item-complete, .nz-file-upload-item-error, .nz-file-upload-item-name, .nz-file-upload-item-size, .nz-file-upload-item-status, .nz-file-upload-list, .nz-file-upload-messages, .nz-file-upload-prompt, .nz-glyph, .nz-progress-bar.

Tokens this stylesheet reads (38)

--nz-bg-brand, --nz-bg-brand-subtle, --nz-bg-disabled, --nz-bg-subtle, --nz-bg-surface, --nz-border-brand, --nz-border-control-hover, --nz-border-critical, --nz-border-strong, --nz-border-subtle, --nz-border-width-default, --nz-duration-instant, --nz-easing-standard, --nz-font-body-m, --nz-font-body-s, --nz-font-body-s-line-height, --nz-font-label-m, --nz-font-label-m-line-height, --nz-font-label-m-strong, --nz-icon-primary, --nz-radius-full, --nz-radius-md, --nz-radius-xl, --nz-size-control-md, --nz-size-icon-md, --nz-size-icon-sm, --nz-space-0-5, --nz-space-1, --nz-space-2, --nz-space-3, --nz-space-6, --nz-text-brand, --nz-text-critical, --nz-text-disabled, --nz-text-on-brand, --nz-text-primary, --nz-text-secondary, --nz-text-success

Accessibility

These parts are interactive. Each gets the focus outline and touch-action rule from @nizivo/react/css/base.css (the nz.base layer), so the floor survives a restyle:

  • file-upload-dropzone: draws the outline when the hidden input inside it has keyboard focus.

In forced-colours mode these states keep a visible system colour: file-upload-dropzone[data-drag-over] (outline).

Keyboard

KeysAction
TabMoves focus to the dropzone's file input; the dropzone draws the focus outline. Tab again moves through each listed file's buttons (Retry, Cancel, Remove) in order.
Enter, SpaceOn the dropzone, opens the platform's file picker (the native input's own behaviour). On a list button, presses it.
Enter, Space on Remove or CancelTakes the file out of the list and the input; focus moves to the file that took its place, else the one before, else the dropzone.

Source: A native <input type="file"> (file-upload.client.tsx); react-aria 3.53.1 useDrop through _engines/react-aria/file-upload.client.tsx for the drop target only. Dropping is pointer-only; the input is the keyboard and screen-reader path. Checked by the browser lane (test/browser/file-upload.spec.tsx) in Chromium.

Figma

Node ids from design/component-map.json, which nz gen writes from component.json. The links open the Nizivo Figma library, which may ask you to sign in.

On this page