# File Field
A field for choosing files to add, with a list showing each file and how it is getting on.
Section: components
Docs: https://nebula.kaluza.com/components/file-field
---
## 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](/components/select-field), [Combobox Field](/components/combobox-field), [Radio](/components/radio-field) or [Checkbox](/components/checkbox-field). File Field is for adding new files.
- **Showing a file the user cannot change** - use [Summary List](/components/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](#what-gets-submitted).
## Anatomy
1. **Label**
2. **Hint text** (optional)
3. **Drop zone**: the upload icon, the prompt, the **Select files** button and the built-in hint, built from `acceptedTypes`, `maxSize` and `maxFiles`
4. **File list**: one card per file, with its name, size, progress bar, status, description and buttons
5. **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.
```tsx
```
File Field does not upload or process anything. To do that, see [Processing files](#processing-files).
## Variants and features
### Several files
Add `multiple` to let users choose more than one file at a time.
```tsx
```
### 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.
```tsx
```
### 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.
```tsx
```
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.
```tsx
const MB = 1024 * 1024;
;
```
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".
```tsx
```
### 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`.
```tsx
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 (
setFileState(({ [id]: _removed, ...rest }) => rest)
}
/>
);
};
```
### 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.
```tsx
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.' },
};
```
### 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.
```tsx
```
### 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](/components/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:
```tsx
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](#processing-files) passes no `onCancel`, so its files have no button until they finish.
```tsx
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 (
setFileState(({ [id]: _removed, ...rest }) => rest)
}
/>
);
};
```
### Controlled and uncontrolled
Pass `files` and `onFilesChange` together to control the list, as in [Processing files](#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](#what-gets-submitted).
Uncontrolled: the field keeps the list, and the app reads it from `onFilesChange`.
```tsx
const Example = () => {
const [names, setNames] = useState([]);
return (
setNames(files.map((file) => file.name))}
/>
);
};
```
Controlled: the app keeps the list, so it can change it, here by clearing it.
```tsx
const Example = () => {
const [files, setFiles] = useState([]);
return (
setFiles([])}>
Clear all files
);
};
```
### 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](/components/form).
With processing that starts as soon as a file is chosen, send what each file produced:
```tsx
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:
```tsx
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](/patterns/forms) for guidance on these topics.
## Rules and guidance
### Usage
- **FileField** MUST always be given an id. Rationale: The label and the Select files button are linked through the id, so the button is named by the label and its own text. [id: filefield.id-required; testability: auto]
- **FileField** WILL keep the list of files the user chose and show the state it is given for each, without uploading or processing anything. Rationale: Processing (an upload, a scan, an import) belongs to the app, so the component never infers a file's state. [id: filefield.no-processing; testability: auto]
- **FileField** MUST provide both files and onFilesChange when used as a controlled component. Rationale: Providing files without onFilesChange creates a list the user cannot add to or remove from. [id: filefield.controlled-pair; testability: auto]
- **FileField** MUST NOT be made unavailable with a disabled state. Rationale: Nebula fields have no disabled state. Hide the field if it does not apply, or show a read-only list in Summary List. [id: filefield.no-disabled; testability: context]
- **FileField** SHOULD NOT be used to choose from a set of existing options. Rationale: Select Field, Combobox Field, Radio and Checkbox are the controls for choosing options. File Field is for adding new files. [id: filefield.not-for-options; testability: context]
### Interaction
- **Processing** SHOULD start from onAdd, which is called once for each new file after onFilesChange. Rationale: The app's list is already up to date when onAdd runs, so each file can be started and tracked by its id. [id: filefield.start-from-onadd; testability: manual]
- **fileState** MUST be keyed by the id of the file, as given in files. Rationale: The 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. [id: filefield.filestate-by-id; testability: auto]
- If onCancel is not passed: **Every file** MUST be settled by the app (complete, warning, failed or cancelled), for example by setting failed after a timeout. Rationale: Without 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. [id: filefield.settle-every-file; testability: manual]
- If onCancel is passed: **Cancel** WILL call onCancel without removing the file, leaving the app to stop the work and to mark the file cancelled or remove it. [id: filefield.cancel-is-app-decided; testability: auto]
### 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 chosen. Rationale: The hint cannot drift from what is enforced, and users learn the limits before they fail them. [id: filefield.limits-in-hint; testability: auto]
- **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 selection. [id: filefield.rejected-files; testability: auto]
- **acceptedTypes** SHOULD list the extension as well for any type the browser may report with a blank MIME type, such as .csv. Rationale: A blank type matches no MIME entry, so a valid file would be rejected. [id: filefield.blank-mime-types; testability: manual]
- **Errors only the app knows** SHOULD be passed through the error prop, or through a Form with a name that matches the field. Rationale: 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. [id: filefield.error-for-app-errors; testability: manual]
- **A form submit** MUST NOT rely on the form, or new FormData(form), to include the files. Rationale: The native file input inside the field is hidden and always empty, so the files are only in the list the field reports. [id: filefield.no-native-form-files; testability: manual]
- **A form submit** MUST be blocked while any file is pending or in progress. Rationale: The field has no submit button of its own, so the app has to stop a form being sent before its files have finished. [id: filefield.block-submit-while-processing; testability: manual]
## Accessibility
### 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
1. Select files
2. 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](https://www.w3.org/WAI/WCAG22/Understanding/headings-and-labels.html)
#### 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](https://www.w3.org/WAI/WCAG22/Understanding/labels-or-instructions.html)
#### 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](https://www.w3.org/WAI/WCAG22/Understanding/error-suggestion.html)
### 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](https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html)
#### 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](https://www.w3.org/WAI/WCAG22/Understanding/error-identification.html)
#### 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](https://www.w3.org/WAI/WCAG22/Understanding/status-messages.html)
#### 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 each test covers:
- 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.
What these statuses mean:
- 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.
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.
## Properties
### FileField
- `id` (type `string`, required). 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` (type `string`, required). Visible label for the field. It is associated with the input, so it forms the input's accessible name.
- `name` (type `string`, optional). 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` (type `ReactNode`, optional). 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` (type `boolean`, optional, default `false`). 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` (type `'always' | 'never' | 'small'`, optional, default `'small'`). 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` (type `number`, optional, default `40`). 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` (type `number`, optional, default `0`). Extra width in pixels added to the width calculated from `characters`, to make room for icons or buttons inside the input.
- `acceptedTypes` (type `Array`, optional). 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` (type `number`, optional). Largest allowed size of a single file, in bytes. For megabytes, multiply: `5 * 1024 * 1024` is 5 MB.
- `files` (type `Array`, optional). The selected files, to control the selection. Use together with `onFilesChange`. Leave unset to let the component keep the selection itself.
- `onFilesChange` (type `(files: Array) => void`, optional). Called with the new list of files whenever files are added or removed. Once per batch, not once per file.
- `onChange` (type `(event: ChangeEvent) => void`, optional). The native change event of the underlying input. Fires first, before the files are validated and added.
- `fileState` (type `Record`, optional). 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` (type `(file: SelectedFile) => void`, optional). Called once for each newly added file, after `onFilesChange`, even within a multi-file batch. Use it to start processing the file.
- `onCancel` (type `(file: SelectedFile) => void`, optional). 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` (type `(file: SelectedFile) => void`, optional). 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` (type `string`, optional). 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` (type `ReactNode`, optional). 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` (type `string`, optional). 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` (type `string`, optional). Class name applied to the outer container.
- `multiple` (type `true | false`, optional, default `false`). Allows selecting more than one file.
- `maxFiles` (type `number | never`, optional). Largest number of files that can be selected in total. Files over the limit are rejected.
### FileInput
- `id` (type `string`, optional). Id of the Select files button. Generated if omitted.
- `name` (type `string`, optional). 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` (type `Array`, optional). 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` (type `number`, optional). Largest allowed size of a single file, in bytes. For megabytes, multiply: `5 * 1024 * 1024` is 5 MB.
- `files` (type `Array`, optional). The selected files, to control the selection. Use together with `onFilesChange`. Leave unset to let the component keep the selection itself.
- `onFilesChange` (type `(files: Array) => void`, optional). Called with the new list of files whenever files are added or removed. Once per batch, not once per file.
- `onChange` (type `(event: ChangeEvent) => void`, optional). The native change event of the underlying input. Fires first, before the files are validated and added.
- `fileState` (type `Record`, optional). 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` (type `(file: SelectedFile) => void`, optional). Called once for each newly added file, after `onFilesChange`, even within a multi-file batch. Use it to start processing the file.
- `onCancel` (type `(file: SelectedFile) => void`, optional). 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` (type `(file: SelectedFile) => void`, optional). 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` (type `string`, optional). 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` (type `ReactNode`, optional). 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` (type `string`, optional). 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` (type `string`, optional). Class name applied to the outer container.
- `multiple` (type `true | false`, optional, default `false`). Allows selecting more than one file.
- `maxFiles` (type `number | never`, optional). Largest number of files that can be selected in total. Files over the limit are rejected.
## Styles
Reference only; you do not need this to use the component. This is the CSS its styled elements use, with theme tokens shown by name. `${...}` is an interpolation (a theme token, a mixin or another value from the code). A `&[prop]` or `@when ` block holds CSS that applies only under that condition; these are notation, not valid CSS.
### card-root
The card, an `li` by default. Its border is the warning or error colour for those statuses.
```css
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
```css
display: flex;
flex-shrink: 0;
font-size: ${space[6]};
color: ${colors.borderMuted};
```
### content
```css
display: flex;
flex: 1 1 0;
flex-direction: column;
gap: ${space[1]};
min-width: 0;
```
### file-name
The file name. Wraps rather than truncating, so the whole name stays visible and in the text.
```css
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
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`.
```css
margin: ${space[1]} 0;
[data-progress-fill] {
background-color: ${$tone === 'neutral' ? colors.border : colors[$tone]};
}
```
### meta-row
```css
display: flex;
flex-wrap: wrap;
gap: ${space[1]} ${space[2]};
```
### small-text
Nebula's `Small` sets the responsive size; the font and colour are set here because `Small` leaves them to the page.
```css
font-family: ${fonts.body};
font-weight: ${fontWeights.normal};
color: ${colors.body};
```
### status
The status line: an optional icon and the status text, set off from the size by a divider.
```css
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
```css
flex-shrink: 0;
width: ${space[4]};
height: ${space[4]};
margin-top: 0.1rem;
```
### description
```css
display: block;
```
### actions
```css
display: flex;
flex-wrap: wrap;
gap: ${space[2]} ${space[6]};
padding-top: ${space[1]};
```
### drop-zone-container
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.
```css
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
```css
width: ${space[6]};
height: ${space[6]};
color: ${colors.body};
flex-shrink: 0;
```
### drop-zone-prompt
```css
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
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.
```css
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
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).
```css
&:empty {
position: absolute;
}
```
### rejection-list
```css
display: flex;
flex-direction: column;
gap: ${space[1]};
margin: 0;
padding: 0;
list-style: none;
```
### file-list
```css
display: flex;
flex-direction: column;
gap: ${space[2]};
margin: 0;
padding: 0;
list-style: none;
&:focus-visible {
${focusRing}
}
```