File Field
A field for choosing files to add, with a list showing each file and how it is getting on.
- Overview
- Rules and guidance
- Properties
- Styles
- Accessibility
When to use
- When users add one or more files as part of a journey, such as a bill, a statement or a photo of a meter
- When you want to show each chosen file, and optionally its progress or result, while your app processes it
- When users need to remove a file again, or retry one that failed
When not to use
- Choosing from a set of existing options - use Select Field, Combobox Field, Radio or Checkbox. File Field is for adding new files.
- Showing a file the user cannot change - use Summary List to show the name and let them download it.
- A form that must send the files itself - File Field does not put its files in the form. Your app sends them, from the list the field reports. See What gets submitted.
Anatomy
- Label
- Hint text (optional)
- Drop zone: the upload icon, the prompt, the Select files button and the built-in hint, built from acceptedTypes, maxSize and maxFiles
- File list: one card per file, with its name, size, progress bar, status, description and buttons
- Error text (optional)
Basic usage
File Field keeps the list of files the user chooses, and shows each one with a button to remove it. Without multiple it holds one file, and choosing another replaces it.
Edit live code
<FileField id="documents" label="Documents" />
File Field does not upload or process anything. To do that, see Processing files.
Variants and features
Several files
Add multiple to let users choose more than one file at a time.
Edit live code
<FileField id="statements" label="Statements" hint="Add one for each account" multiple />
Limiting the number of files
maxFiles is the most files the field holds in total, counting those already chosen, and needs multiple. The built-in hint states it ("Up to 3 files."), and a file over the limit is not added.
Edit live code
<FileField id="bills" label="Recent bills" multiple maxFiles={3} />
Limiting file types
acceptedTypes takes MIME types ('application/pdf'), wildcards ('image/*') or extensions ('.docx'). The built-in hint lists them ("Accepted files: PDF, PNG."), and a file of another type is not added.
Edit live code
<FileField id="proof" label="Proof of address" multiple acceptedTypes={['application/pdf', 'image/png']} />
Browsers sometimes leave a file's type blank (a .csv, for example), and a blank type matches no MIME entry. List the extension too for any such type.
Limiting file size
maxSize is the largest size of one file, in bytes, so write megabytes as a multiplication: 5 * 1024 * 1024 is 5 MB. Keep the multiplier in a named constant if you use it more than once.
const MB = 1024 * 1024;
<FileField id="photo" label="Photo of the meter" maxSize={2 * MB} />;
The built-in hint shows the size in the largest unit that fits, counting 1024 bytes to a KB and 1024 KB to a MB, so maxSize={1.5 * MB} reads "1.5 MB".
Edit live code
<FileField id="photo" label="Photo of the meter" acceptedTypes={['image/*']} maxSize={2 * 1024 * 1024} />
Processing files
File Field does not upload or process anything. It keeps the list and shows the state you give it for each file. Start the work from onAdd, which is called once for each new file, and report how each one is getting on through fileState, keyed by the file's id.
Edit live code
() => { // ignore - for live code example only. const Example = () => { const [files, setFiles] = useState([]); const [fileState, setFileState] = useState({}); const update = (id, next) => setFileState((current) => ({ ...current, [id]: next })); // Stands in for your upload: reports progress, then completes. const start = (selected) => { const tick = (progress) => { if (progress >= 100) { update(selected.id, { status: 'complete' }); return; } update(selected.id, { status: 'in-progress', progress }); setTimeout(() => tick(progress + 25), 500); }; tick(0); }; return ( <FileField id="documents" label="Documents" hint="Bills, statements or meter photos" multiple maxFiles={4} acceptedTypes={['application/pdf', 'image/png']} maxSize={5 * 1024 * 1024} files={files} onFilesChange={setFiles} fileState={fileState} onAdd={start} onRemove={({ id }) => setFileState(({ [id]: _removed, ...rest }) => rest) } /> ); }; return <Example />; }
Showing progress and results
fileState is optional and is never worked out by the component. A file with no entry is a plain row with just Remove. Each entry has a status (pending, in-progress, complete, warning, failed or cancelled), an optional progress from 0 to 100 (leave it out for an indeterminate bar), a description for the reason or next step, and one extra action, such as Retry on a failed file.
Edit live code
() => { // ignore - for live code example only. const files = [ { id: 'a', file: null, name: 'bill.pdf', size: 120 * 1024 }, { id: 'b', file: null, name: 'statement.pdf', size: 2.4 * 1024 * 1024 }, { id: 'c', file: null, name: 'scan.pdf', size: 12 * 1024 * 1024 }, { id: 'd', file: null, name: 'meter.png', size: 800 * 1024 }, ]; const fileState = { a: { status: 'complete' }, b: { status: 'in-progress', progress: 40 }, c: { status: 'failed', description: 'The file is too large. Choose one under 10 MB.', action: { id: 'retry', label: 'Retry', onSelect: () => undefined }, }, d: { status: 'warning', description: 'Some details could not be read.' }, }; return ( <FileField id="documents-states" label="Documents" multiple files={files} fileState={fileState} /> ); }
Progress you cannot measure
Leave progress out (or null) while a file is pending or in-progress, and its card shows an indeterminate bar that keeps moving until you report a status. Use it when your work gives no progress, such as a fetch upload.
Edit live code
<FileField id="indeterminate" label="Documents" multiple files={[{ id: 'a', file: null, name: 'bill.pdf', size: 120 * 1024 }]} fileState={{ a: { status: 'in-progress' } }} />
Errors on files and on the field
An error belongs either to one file or to the field as a whole.
An error on a file is about that file, and sits on its card. Set status: 'failed' with a description that says what went wrong and what to do about it, and an optional action such as Retry. A file the field refuses (the wrong type, too big, or over the limit) never gets a card: a message at the drop zone names it and says why, and stays until the next selection. The field writes that message itself.
An error on the field is about what only your app knows, such as "Add at least one document" or "Add a statement for each account". Pass an error message, or put the field in a Form and give it a name that matches the form error. The message appears between the drop zone and the file list, so it stays next to the button it describes however many files there are. The drop zone gets an error border, and the message is read out with the Select files button.
Use a file's own error for a problem the user can fix on that file, and the field's error for the field as a whole. Do not repeat a file's failure as a field error: the card already says it. The field's error does not clear itself, so remove it once the user has put it right. Both can show at once:
Edit live code
<FileField id="documents-errors" label="Documents" hint="Bills, statements or meter photos" multiple files={[ { id: 'a', file: null, name: 'bill.pdf', size: 120 * 1024 }, { id: 'b', file: null, name: 'scan.pdf', size: 12 * 1024 * 1024 }, ]} fileState={{ a: { status: 'complete' }, b: { status: 'failed', description: 'The file is too large. Choose one under 10 MB.', action: { id: 'retry', label: 'Retry', onSelect: () => undefined }, }, }} error="Add a statement for each account" />
Cancelling files in flight
A file that is pending or in-progress shows Cancel only if you pass onCancel. Cancelling does not remove the file: your app decides whether to stop the work and mark the file cancelled, or to remove it. If your processing cannot be cancelled, pass no onCancel. There is then no button until the file settles, so make sure every file does, for example by setting failed after a timeout.
In this example the work can be stopped, so it passes onCancel. Choose a file, then cancel it while it is in progress; the file stays in the list as cancelled, with Remove. The example under Processing files passes no onCancel, so its files have no button until they finish.
Edit live code
() => { // ignore - for live code example only. const Example = () => { const [files, setFiles] = useState([]); const [fileState, setFileState] = useState({}); const [timers] = useState({}); const update = (id, next) => setFileState((current) => ({ ...current, [id]: next })); // Stands in for work that can be stopped. const start = (selected) => { const tick = (progress) => { if (progress >= 100) { update(selected.id, { status: 'complete' }); return; } update(selected.id, { status: 'in-progress', progress }); timers[selected.id] = setTimeout(() => tick(progress + 10), 800); }; tick(0); }; const cancel = (selected) => { clearTimeout(timers[selected.id]); update(selected.id, { status: 'cancelled' }); }; return ( <FileField id="cancellable" label="Documents" multiple files={files} onFilesChange={setFiles} fileState={fileState} onAdd={start} onCancel={cancel} onRemove={({ id }) => setFileState(({ [id]: _removed, ...rest }) => rest) } /> ); }; return <Example />; }
Controlled and uncontrolled
Pass files and onFilesChange together to control the list, as in Processing files. Leave files out and the field keeps the list itself; read it from onFilesChange (once per batch) or onAdd (once per file). onChange is the native change event, called first. The field's own input is never a way to read the files: it is always empty. See What gets submitted.
Uncontrolled: the field keeps the list, and the app reads it from onFilesChange.
Edit live code
() => { // ignore - for live code example only. const Example = () => { const [names, setNames] = useState([]); return ( <Stack spaceBetween={3}> <FileField id="uncontrolled" label="Documents" multiple onFilesChange={(files) => setNames(files.map((file) => file.name))} /> <P>Chosen: {names.length ? names.join(', ') : 'none'}</P> </Stack> ); }; return <Example />; }
Controlled: the app keeps the list, so it can change it, here by clearing it.
Edit live code
() => { // ignore - for live code example only. const Example = () => { const [files, setFiles] = useState([]); return ( <Stack spaceBetween={3}> <FileField id="controlled" label="Documents" multiple files={files} onFilesChange={setFiles} /> <SecondaryCTAButton onClick={() => setFiles([])}> Clear all files </SecondaryCTAButton> </Stack> ); }; return <Example />; }
What gets submitted
File Field does not put anything in a form. The native file input inside it is hidden and cleared after every choice, and dropped files never reach it, so a native form post, or new FormData(form), has no files for this field. Build what you send from the list the field reports, as with the other fields in a Form.
With processing that starts as soon as a file is chosen, send what each file produced:
const attachments = files
.map((file) => fileState[file.id]?.payload)
.filter((payload) => payload != null);
If nothing happens until submit, read the File objects from files and build your own request. Either way, stop the form submitting while any file is still being processed:
const isProcessing = Object.values(fileState).some(
(state) => state.status === 'pending' || state.status === 'in-progress',
);
Common form features
File Field shares validation, hint text, required/optional marking and field width behaviour with other form fields. See the forms pattern guide for guidance on these topics.
usage
- FileField must always be given an idThe label and the Select files button are linked through the id, so the button is named by the label and its own text.Copy rule ID for filefield.id-required
- FileField will keep the list of files the user chose and show the state it is given for each, without uploading or processing anythingProcessing (an upload, a scan, an import) belongs to the app, so the component never infers a file's state.Copy rule ID for filefield.no-processing
- FileField must provide both files and onFilesChange when used as a controlled componentProviding files without onFilesChange creates a list the user cannot add to or remove from.Copy rule ID for filefield.controlled-pair
- FileField must not be made unavailable with a disabled stateNebula fields have no disabled state. Hide the field if it does not apply, or show a read-only list in Summary List.Copy rule ID for filefield.no-disabled
- FileField should not be used to choose from a set of existing optionsSelect Field, Combobox Field, Radio and Checkbox are the controls for choosing options. File Field is for adding new files.Copy rule ID for filefield.not-for-options
interaction
- Processing should start from onAdd, which is called once for each new file after onFilesChangeThe app's list is already up to date when onAdd runs, so each file can be started and tracked by its id.Copy rule ID for filefield.start-from-onadd
- fileState must be keyed by the id of the file, as given in filesThe id is the only link between a file and its state. A file with no entry is shown as a plain row with just Remove.Copy rule ID for filefield.filestate-by-id
- If onCancel is not passedEvery file must be settled by the app (complete, warning, failed or cancelled), for example by setting failed after a timeoutWithout onCancel there is no Cancel button while a file is pending or in progress, and Remove only appears once it settles. A file that never finishes cannot be dismissed.Copy rule ID for filefield.settle-every-file
- If onCancel is passedCancel will call onCancel without removing the file, leaving the app to stop the work and to mark the file cancelled or remove itCopy rule ID for filefield.cancel-is-app-decided
validation
- Built-in hint will state the accepted types, the maximum size and the maximum number of files, built from acceptedTypes, maxSize and maxFiles, before any file is chosenThe hint cannot drift from what is enforced, and users learn the limits before they fail them.Copy rule ID for filefield.limits-in-hint
- Files that break a limit will not be added, with a message at the drop zone that names each one and says why, until the next selectionCopy rule ID for filefield.rejected-files
- acceptedTypes should list the extension as well for any type the browser may report with a blank MIME type, such as .csvA blank type matches no MIME entry, so a valid file would be rejected.Copy rule ID for filefield.blank-mime-types
- Errors only the app knows should be passed through the error prop, or through a Form with a name that matches the fieldThis links the message to the Select files button and shows it between the drop zone and the file list, next to the button it describes. Text placed beside the field is not read out with it.Copy rule ID for filefield.error-for-app-errors
- A form submit must not rely on the form, or new FormData(form), to include the filesThe native file input inside the field is hidden and always empty, so the files are only in the list the field reports.Copy rule ID for filefield.no-native-form-files
- A form submit must be blocked while any file is pending or in progressThe field has no submit button of its own, so the app has to stop a form being sent before its files have finished.Copy rule ID for filefield.block-submit-while-processing
FileField
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
id | string | — | Yes | Unique id for the input, used to link the label, hint and error to it. Also used as the base for related ids such as `{id}-error`. Id of the Select files button. Generated if omitted. |
label | string | — | Yes | Visible label for the field. It is associated with the input, so it forms the input's accessible name. |
name | string | — | No | The `name` attribute passed to the input. Also used to look up the field's error from a surrounding `Form`. Name of the native file input. The input is hidden and always empty, so the files are not sent with a form: build what you send from `files` and `fileState`. |
hint | ReactNode | — | No | Helper text shown below the label. It is part of the label element, so it is read as part of the input's accessible name. |
optional | boolean | false | No | Marks the field as not required. This appends "(optional)" to the label (or, when the theme marks required fields, hides the required asterisk) and sets `aria-required` on the input only when the field is not optional. |
fullWidth | always never small | 'small' | No | Controls when the field stretches to the full width of its container. `always` is always full width, `never` always uses the width set by `characters`, and `small` is full width on small screens only. |
characters | number | 40 | No | Width of the input, as an approximate number of characters (the width is set to `0.8em` per character, rounded up). Does not limit how much can be typed. |
affixWidth | number | 0 | No | Extra width in pixels added to the width calculated from `characters`, to make room for icons or buttons inside the input. |
acceptedTypes | Array<AcceptedFileType> | — | No | File types to accept, as MIME types (`'image/png'`), wildcards (`'image/*'`) or extensions (`'.pdf'`). Sets the native `accept` attribute, is shown in the hint text and is checked against both selected and dropped files. |
maxSize | number | — | No | Largest allowed size of a single file, in bytes. For megabytes, multiply: `5 * 1024 * 1024` is 5 MB. |
files | Array<SelectedFile> | — | No | The selected files, to control the selection. Use together with `onFilesChange`. Leave unset to let the component keep the selection itself. |
onFilesChange | (files: Array<SelectedFile>) => void | — | No | Called with the new list of files whenever files are added or removed. Once per batch, not once per file. |
onChange | (event: ChangeEvent<HTMLInputElement>) => void | — | No | The native change event of the underlying input. Fires first, before the files are validated and added. |
fileState | Record<string, FileState> | — | No | Processing state for each file, keyed by `SelectedFile.id`. A file with no entry is shown as a plain row with just Remove. Never inferred by the component. |
onAdd | (file: SelectedFile) => void | — | No | Called once for each newly added file, after `onFilesChange`, even within a multi-file batch. Use it to start processing the file. |
onCancel | (file: SelectedFile) => void | — | No | Called when Cancel is selected on a `pending` or `in-progress` file. The file is not removed; the app decides what happens to it. Without it there is no Cancel button for a file in flight (for processing that cannot be cancelled), so the app must settle every file, for example by setting `failed` after a timeout. |
onRemove | (file: SelectedFile) => void | — | No | Called when Remove is selected on a settled file (or one with no `fileState`), and, without `multiple`, for the file that choosing another one replaces (after `onFilesChange`, before `onAdd` for the new file). Without `files`, the component removes the file itself; with it, the app must update `files`. |
aria-describedby | string | — | No | Id of an element that describes the input, such as an error message from a surrounding field. Read out after the built-in hint text. |
error | ReactNode | — | No | An error about the whole input, such as "Add at least one document". It is shown between the drop zone and the file list, the drop zone gets an error border, and the message is linked to the Select files button with `aria-describedby`, after the built-in hint and any message about a rejected file. The button is not marked `aria-invalid`, which is not valid on a button. An error about one file belongs in that file's `fileState` instead. |
aria-labelledby | string | — | No | Ids of the elements that name the Select files button, such as a surrounding field's label, followed by the button's own id so that its text ("Select files") stays in its name. |
className | string | — | No | Class name applied to the outer container. |
multiple | true | false | false | No | Allows selecting more than one file. |
maxFiles | number | never | — | No | Largest number of files that can be selected in total. Files over the limit are rejected. |
FileInput
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
id | string | — | No | Id of the Select files button. Generated if omitted. |
name | string | — | No | Name of the native file input. The input is hidden and always empty, so the files are not sent with a form: build what you send from `files` and `fileState`. |
acceptedTypes | Array<AcceptedFileType> | — | No | File types to accept, as MIME types (`'image/png'`), wildcards (`'image/*'`) or extensions (`'.pdf'`). Sets the native `accept` attribute, is shown in the hint text and is checked against both selected and dropped files. |
maxSize | number | — | No | Largest allowed size of a single file, in bytes. For megabytes, multiply: `5 * 1024 * 1024` is 5 MB. |
files | Array<SelectedFile> | — | No | The selected files, to control the selection. Use together with `onFilesChange`. Leave unset to let the component keep the selection itself. |
onFilesChange | (files: Array<SelectedFile>) => void | — | No | Called with the new list of files whenever files are added or removed. Once per batch, not once per file. |
onChange | (event: ChangeEvent<HTMLInputElement>) => void | — | No | The native change event of the underlying input. Fires first, before the files are validated and added. |
fileState | Record<string, FileState> | — | No | Processing state for each file, keyed by `SelectedFile.id`. A file with no entry is shown as a plain row with just Remove. Never inferred by the component. |
onAdd | (file: SelectedFile) => void | — | No | Called once for each newly added file, after `onFilesChange`, even within a multi-file batch. Use it to start processing the file. |
onCancel | (file: SelectedFile) => void | — | No | Called when Cancel is selected on a `pending` or `in-progress` file. The file is not removed; the app decides what happens to it. Without it there is no Cancel button for a file in flight (for processing that cannot be cancelled), so the app must settle every file, for example by setting `failed` after a timeout. |
onRemove | (file: SelectedFile) => void | — | No | Called when Remove is selected on a settled file (or one with no `fileState`), and, without `multiple`, for the file that choosing another one replaces (after `onFilesChange`, before `onAdd` for the new file). Without `files`, the component removes the file itself; with it, the app must update `files`. |
aria-describedby | string | — | No | Id of an element that describes the input, such as an error message from a surrounding field. Read out after the built-in hint text. |
error | ReactNode | — | No | An error about the whole input, such as "Add at least one document". It is shown between the drop zone and the file list, the drop zone gets an error border, and the message is linked to the Select files button with `aria-describedby`, after the built-in hint and any message about a rejected file. The button is not marked `aria-invalid`, which is not valid on a button. An error about one file belongs in that file's `fileState` instead. |
aria-labelledby | string | — | No | Ids of the elements that name the Select files button, such as a surrounding field's label, followed by the button's own id so that its text ("Select files") stays in its name. |
className | string | — | No | Class name applied to the outer container. |
multiple | true | false | false | No | Allows selecting more than one file. |
maxFiles | number | never | — | No | Largest number of files that can be selected in total. Files over the limit are rejected. |
Styles
card-root
Defined in Input/FileInput/FileCard/FileCard.tsx
The card, an `li` by default. Its border is the warning or error colour for those statuses.
display: flex;
align-items: flex-start;
gap: ${space[2]};
margin: 0;
padding: ${space[2]};
list-style: none;
background-color: ${colors.canvas};
border: ${borderWidths.standard} solid ${$tone === 'warning' ? colors.warning : $tone === 'error' ? colors.error : colors.borderMuted};
border-radius: ${radii.standard};
file-icon-wrapper
Defined in Input/FileInput/FileCard/FileCard.tsx
display: flex;
flex-shrink: 0;
font-size: ${space[6]};
color: ${colors.borderMuted};
content
Defined in Input/FileInput/FileCard/FileCard.tsx
display: flex;
flex: 1 1 0;
flex-direction: column;
gap: ${space[1]};
min-width: 0;
file-name
Defined in Input/FileInput/FileCard/FileCard.tsx
The file name. Wraps rather than truncating, so the whole name stays visible and in the text.
font-family: ${fonts.body};
font-weight: ${fontWeights.bold};
font-size: ${responsiveFontSizes.body[0]};
color: ${colors.body};
overflow-wrap: anywhere;
${mediaQueries.smallAndUp} {
font-size: ${responsiveFontSizes.body[1]};
}
card-progress-bar
Defined in Input/FileInput/FileCard/FileCard.tsx
A `ProgressBar` for this card only. `ProgressBar` has just a primary colour, so this recolours the fill through its internal `data-progress-fill` hook (not public API) rather than adding a colour prop to `ProgressBar`.
margin: ${space[1]} 0;
[data-progress-fill] {
background-color: ${$tone === 'neutral' ? colors.border : colors[$tone]};
}
meta-row
Defined in Input/FileInput/FileCard/FileCard.tsx
display: flex;
flex-wrap: wrap;
gap: ${space[1]} ${space[2]};
small-text
Defined in Input/FileInput/FileCard/FileCard.tsx
Nebula's `Small` sets the responsive size; the font and colour are set here because `Small` leaves them to the page.
font-family: ${fonts.body};
font-weight: ${fontWeights.normal};
color: ${colors.body};
status
Defined in Input/FileInput/FileCard/FileCard.tsx
The status line: an optional icon and the status text, set off from the size by a divider.
display: inline-flex;
align-items: flex-start;
gap: ${space[1]};
padding-left: ${space[2]};
border-left: ${borderWidths.standard} solid ${colors.border};
color: ${$tone === 'neutral' ? colors.body : colors[$tone]};
status-icon
Defined in Input/FileInput/FileCard/FileCard.tsx
flex-shrink: 0;
width: ${space[4]};
height: ${space[4]};
margin-top: 0.1rem;
actions
Defined in Input/FileInput/FileCard/FileCard.tsx
display: flex;
flex-wrap: wrap;
gap: ${space[2]} ${space[6]};
padding-top: ${space[1]};
drop-zone-container
Defined in Input/FileInput/FileDropZone/FileDropZone.tsx
Surrounds the icon, prompt, "Select files" button and hint. Dashed border by default, a solid heavier error border when invalid, and a tinted fill and focus-coloured border while a drag is over it.
display: flex;
flex-direction: column;
align-items: center;
gap: ${space[2]};
min-width: min(32rem, 100%);
padding: ${space.cardPadding};
border: ${borderWidths.standard} dashed ${colors.borderMuted};
border-radius: ${radii.standard};
background: ${colors.canvasMuted};
text-align: center;
transition: background ${transitions.standard}, border-color ${transitions.standard};
&:has(input:focus-visible) {
border-color: ${colors.focus};
}
@when $invalid {
border: ${borderWidths.heavy} solid ${colors.error};
}
@when $dragActive {
border-color: ${colors.focus};
background: ${colors.primaryMuted};
${focusRing}
}
drop-zone-icon
Defined in Input/FileInput/FileDropZone/FileDropZone.tsx
width: ${space[6]};
height: ${space[6]};
color: ${colors.body};
flex-shrink: 0;
drop-zone-prompt
Defined in Input/FileInput/FileDropZone/FileDropZone.tsx
display: flex;
flex-direction: column;
align-items: center;
gap: ${space[1]};
color: ${colors.body};
font-family: ${fonts.body};
line-height: ${lineHeights.body};
font-size: ${responsiveFontSizes.body[0]};
${mediaQueries.smallAndUp} {
font-size: ${responsiveFontSizes.body[1]};
}
strong {
font-weight: ${fontWeights.bold};
}
select-files-button
Defined in Input/FileInput/FileDropZone/FileDropZone.tsx
The "Select files" button, styled to match the secondary button. It is the one focusable, announced control; clicking it opens the file picker of the hidden native input. The input is kept out of the accessibility tree because a focused file input re-announces its value ("No file chosen") whenever it is cleared, which contradicts the files that were just added.
display: inline-flex;
align-items: center;
justify-content: center;
min-height: ${space[9]};
padding: ${space[2]} ${space[3]};
border: ${borderWidths.standard} solid ${colors.primary};
border-radius: ${radii.standard};
background: ${colors.primaryContrast};
color: ${colors.primary};
font-family: ${fonts.display};
font-weight: ${fontWeights.bold};
line-height: ${lineHeights.body};
font-size: ${responsiveFontSizes.body[0]};
cursor: pointer;
transition: ${transitions.standard};
${mediaQueries.smallAndUp} {
font-size: ${responsiveFontSizes.body[1]};
}
&:hover {
background: ${colors.primaryMuted};
border-color: ${colors.primaryAlt};
color: ${colors.primaryAlt};
}
&:focus-visible {
${focusRing}
}
rejections-region
Defined in Input/FileInput/FileDropZone/FileDropZone.tsx
The live region that rejection messages are added to. It is always rendered so assistive technology announces what is added to it, which means it is a flex item even when empty, and the container's `gap` would leave a blank strip under the last visible element. While empty it is taken out of the layout (`display: none` would also take it out of the accessibility tree, and nothing is announced from a region that was not there first).
&:empty {
position: absolute;
}
rejection-list
Defined in Input/FileInput/FileDropZone/FileDropZone.tsx
display: flex;
flex-direction: column;
gap: ${space[1]};
margin: 0;
padding: 0;
list-style: none;
file-list
Defined in Input/FileInput/FileList/FileList.tsx
display: flex;
flex-direction: column;
gap: ${space[2]};
margin: 0;
padding: 0;
list-style: none;
&:focus-visible {
${focusRing}
}
In brief
File Field is a field with a Select files button, a drop zone and a list of the files the user has chosen. The button is named by the field's label and hint followed by its own text, and the list is announced as "Selected files". A single status message announces files added, finished, failed, cancelled, removed and rejected, and never announces progress.
What Nebula provides
- A real label linked to the button, and aria-labelledby naming the button with the label (and its hint) followed by its own text, so the visible words "Select files" stay in its name.
- The button is described by the built-in hint (the accepted types, size and number of files), then by any message about a rejected file, then by the field's error.
- The files are a list named "Selected files", and each button names its file after the visible text, as in "Remove bill.pdf". A failure reason is linked to the buttons.
- One status message, a role="status" region that exists from the first render, announces changes to the files, combined when they land together.
Keyboard behaviour
Tab reaches the Select files button, then the file list, which is a single tab stop; the arrow keys move between the files and their buttons. After files are chosen with the button, focus moves to the file list, so a screen reader reads "Selected files, list" and the number of files instead of reading the button and its hint again when the file dialog closes. Dropping files does not move focus.
Focus order
- Select files
- The current button in the file list (the others are reached with the arrow keys)
Interactions
- Tab - Moves focus to Select files, then to the file list
- Shift+Tab - Moves focus back to Select files from the file list
- Up or Down - Moves to the same button in the previous or next file
- Left or Right - Moves between the buttons of one file
- Home or End - Moves to the first button of the first or last file
- Down, Right or Home on the file list itself - Moves into the files after they have been chosen
Accessible design
Write a label that names what the user is adding
The label is read together with the hint and the words "Select files" as the name of the button, so name the files, as in "Proof of address", and not the action, as in "Upload". The button already says what to do.
WCAG: 2.4.6 Headings and Labels
Don't repeat the limits in the hint
The built-in hint already states the accepted types, the maximum size and the maximum number of files, and it cannot drift from what is enforced. Use the hint for what only you know, such as which documents are accepted as proof.
WCAG: 3.3.2 Labels or Instructions
Give every failure a reason and a next step
A file that fails shows its description and can show one button, such as Retry. Say what went wrong and what to do about it: "The file is too large. Choose one under 10 MB." beats "Upload failed".
WCAG: 3.3.3 Error Suggestion
Accessible development
Always pass an id
The label and the Select files button are linked through it. Without it the button cannot be named by the label.
WCAG: 1.3.1 Info and Relationships
Always pass errors through error, or through a Form with the field name
This links the message to the Select files button and shows it between the drop zone and the file list, next to the button it describes. Text placed beside the field is not read out with it.
WCAG: 3.3.1 Error Identification
Don't announce file changes a second time
The field announces files added, finished, failed, cancelled, removed and rejected. A second message from your app, such as a toast for each completed file, is read out on top of it.
WCAG: 4.1.3 Status Messages
If you pass no onCancel, settle every file
Without onCancel there is no Cancel button while a file is pending or in progress, and Remove only appears once it settles. Set failed after a timeout so a file that never finishes can still be dismissed.
Accessibility testing status
Reviewed on 13 October 2026 against v17.0.1.
| Test | Status | Tested with |
|---|---|---|
| Default state | Tested | axe-core via the Storybook test runner (CI) |
| Advanced states | Partially tested | axe-core via the Storybook test runner (CI) |
| Keyboard navigation | Manually tested | Chrome on macOS with VoiceOver on (reported by the owner), VoiceOver with Chrome (reported by the owner) |
| Screen reader | Partially tested | VoiceOver with Chrome (reported by the owner) |
What these tests and statuses mean
The table records which checks were run on this component on its own. It is not a statement of WCAG conformance, and it does not cover how the component is used.
The tests
- Default state: The component as it first appears, before anything is clicked or changed.
- Advanced states: Every other state and variant: error, open, selected, each size and style, and hovering.
- Keyboard navigation: Whether every function can be reached and used with the keyboard alone, with a visible focus ring.
- Screen reader: Whether a screen reader announces what is on screen correctly: the name, the role and any state.
The statuses
- Tested: Every check in this row passed in an automated run that repeats on every pull request.
- Manually tested: Every check in this row was run once, by a person or with a tool they started, and all of them passed. It is not re-run on every change.
- Partially tested: Some checks in this row were run once and passed; the rest are still to do.
- Issue found: A check in this row failed. The ticket is shown beside the status.
- Not tested: No check in this row has been run yet.
- Not applicable: This row does not apply to the component, for the reason given beside it.