A place to choose files or drop them, with what is accepted said first, and each chosen file in a row below with its progress and result.
Choose files or drag them here
PDF or Word, up to 10 MB each, 5 files at most.
Options
| Choice | Values | Default | What it does |
|---|---|---|---|
typesmbk-file-upload--type-<value> | drop-zonebutton-only | drop-zone | A drop zone is a dashed area with a title, what is accepted and a button, that also takes files dropped on it; button only is the button over what is accepted, for narrow places. Figma calls them "Drop zone" and "Button only". |
errorsmbk-file-upload--error | On or off | Off | Something is wrong, such as no file chosen when one is required. The zone takes the danger edge; say what is wrong in the message. |
draggingsmbk-file-upload--dragging | On or off | Off | Files are held over the zone: the brand edge, the sunken surface and "Drop to add", at the same height. The wrapper sets it while dragging; Figma calls it "Drag-over". |
Examples
Usage
Press Browse files, or drop files on the zone. Each file gets a row that shows its progress, then whether it went through; a file of the wrong type or too large is not added, and says why.
Choose files or drag them here
PDF or Word, up to 10 MB each, 5 files at most.
quarterly-report.pdf
board-minutes.docx
signed-contract.pdf
Button only
The button over what is accepted, for narrow places such as a phone, where nothing is dragged.
One file
For one file the words say so, and a new choice takes the old one’s place.
Choose a file or drag it here
PDF or Word, up to 10 MB.
signed-contract.pdf
Drag over
While files are held over it, the zone turns its edge brand and its surface sunken and says “Drop to add”, at the same height.
Choose files or drag them here
PDF or Word, up to 10 MB each, 5 files at most.
Error
The danger edge, and the message in the zone under the button, saying what to do.
Choose files or drag them here
PDF or Word, up to 10 MB each, 5 files at most.
Disabled
Dimmed and out of reach, the button with it.
Choose files or drag them here
PDF or Word, up to 10 MB each, 5 files at most.
Guidelines
Purpose
Lets the reader add files to a form or a library: by pressing a button that opens the browser's own file picker, or by dropping files on a zone. It says what is accepted before anyone chooses, and shows each chosen file in a row under it with its progress, then whether it went through. A plain file input shows none of that.
When to use
- A form that takes documents, such as a claim with its receipts or an application with its papers.
- A library or a workspace where files are added and uploaded one by one.
- A single file, such as a picture for a profile or a signed contract: set it to take one.
When not to use
- A choice among files already on the server, such as a picture from a library: that is a picker of its own.
- Text that could be typed or pasted into a text area.
- A narrow place such as a phone, for the drop zone: nothing is dragged there. Use the button-only form.
Usage rules
Do
- Say what is accepted in the constraints: the types, the largest size, how many.
- Put it in a field, so the input is named.
- Say why a file was not added, in its row: "Not added: it must be a PDF."
- Use the button-only form in narrow places.
Don't
- Make dropping the only way to add a file.
- Trust the file's extension or type: the server checks what it takes.
- Read out every percent of an upload.
Choose files or drag them here
PDF or Word, up to 10 MB each, 5 files at most.
Say what is accepted before anyone chooses: the types, the largest size, how many.
Choose files or drag them here
Without the line the reader learns the rules only when a file is refused.
In a narrow place, such as a phone, use the button-only form: nothing is dragged there.
Choose a file or drag it here
A photo, up to 5 MB.
A drop zone on a phone takes room for a way of adding files no one can use there.
Anatomy
- The block, a column: the zone, then the list of files, then the announcement part.
- The zone part. In a drop zone it is a dashed area at the medium container corners; in the button-only form it draws nothing of its own. It holds the body part and, in a drop zone, the drop part, in one place, so the zone is as tall as its body whichever shows.
- The body part. In a drop zone: the icon part (an upload arrow), the text part with the title part over the constraints part, the button part, and in error the message part. In the button-only form: the button, the constraints and the message, stacked from the start edge.
- The button part, a label round the input carrying the button's secondary classes at the medium size, so its looks are the button's.
- The input part, the native file input inside the button: in the page and the tab order, unseen.
- The constraints part: what is accepted, said before anyone chooses: the types, the largest size, how many.
- The message part, a field message of the error type: what is wrong and what to do.
- The drop part, shown in the body's place while files are held over the zone: the icon, "Drop to add" and the constraints, centred. It is hidden from assistive technology.
- The files part, a list of the chosen files; each file part holds one file row.
- The announcement part, a status region that is in the page but not drawn.
- The type is a modifier:
--type-drop-zone,--type-button-only. The--errorand--draggingflags are modifiers too. - The label is not the upload's: put it in a field.
Behavior
The button opens the browser's own file picker, with or without script. Pressing it, or Enter or Space when the input has focus, opens the picker. A drop zone also takes files dropped anywhere on it. While files are held over it, its edge turns the brand color at the emphasis width, its surface turns sunken and its words turn to "Drop to add", and it keeps its height. A file dropped on a disabled upload is ignored, and is not opened in the page's place. The button-only form takes no drop.
The React wrapper uploads nothing. It hands what was chosen or dropped to onAdd, and draws a file row for each file in files, which the page keeps and updates as each upload goes. It passes each row's buttons on as onRemove, onCancel, onRetry and onDelete, with the file's id. With accept, a file of another type is not added: it is passed to onAdd in the refused list with the reason type, so the page can show it as not added and say why. Without multiple, the upload takes one file, and a second one dropped at the same time is refused with the reason count. The input is emptied after each choice, so the same file can be chosen again after it is removed. It is required only while no file is chosen, so a form with a required upload is not sent empty.
In error the zone draws the danger edge at the emphasis width; errorMessage shows the message in the zone, under the button, or under the constraints in the button-only form. Give the message to the upload, not to the field around it: the field would draw it a second time under the zone. The upload's error turns on the error look without a message. Disabled, the icon and the words take the disabled colors and the button its own disabled look. Read-only (readOnly, which a field passes on), the upload shows its files and takes no more: a browser has no read-only file input, so the button and the zone are drawn and behave as disabled, and the rows show without their buttons. A ref given to the React wrapper reaches the native input.
Accessibility
What it does for you
- The input is named by the field's label, then the button's words ("Attachments Browse files"), so a reader using voice control can say either. The constraints and the error message are its description, read before anyone chooses.
- Tab reaches the input, which the focus ring round the zone shows (round the button alone in the button-only form), then each row's buttons. Enter or Space opens the picker.
- Dropping is an extra: the button always works.
- Each row's buttons are named with the file: "Remove quarterly-report.pdf", "Cancel upload of quarterly-report.pdf", "Try again to upload quarterly-report.pdf", "Delete quarterly-report.pdf".
- A file's change of status, a file added and a file removed are read out once each, with the file's name, in one polite status region: "quarterly-report.pdf: Uploaded". The progress in between is not read out.
- When the button that had focus goes, as when a file is removed, focus moves to the next row's first button, else to the last row's, else to the input.
- Disabled or read-only, the zone carries
aria-disabledbeside the input'sdisabled, so its faint title and line are read as part of a control out of reach.
What you must do
- Put the upload in a field, so the input has a name, or give it an
aria-label. - Say what is accepted in the constraints.
acceptand the constraints are hints for the reader; the server decides what it takes. - Change the words for another language:
title,dropTitle,buttonLabel,addedLabel,removedLabel, the row's button names andsizeLabel, andlocalefor the sizes. - In markup written by hand, add
aria-disabledanddisabledtogether when it is disabled or read-only, and leave out the rows' buttons when read-only.
How to test
- With the keyboard only, Tab to the upload: the focus ring shows round the zone (or the button). Press Enter, then Space: each opens the picker.
- Add a file, then Tab to its row's buttons and remove it: focus moves to the next row's first button, else the last row's, else the input.
- With a screen reader, the input is read by the field's label and the button's words, with the constraints; each row's buttons are read with the file's name.
- Add, finish and remove a file: each change is read out once, and the progress in between is not.
- With text at 200% and the page 320 pixels wide, use the button-only form: the button and the rows stay whole.
- In a forced-color mode, the zone's edge, the focus ring and the buttons are still visible.
Keyboard
| Key | What it does |
|---|---|
| Tab | Moves focus to the input, then to each row's buttons. |
| EnterorSpace | On the input, opens the picker. On a row's button, presses it. |
Source
The files the file upload is made of. Its folder on GitHub (opens in a new tab) holds them with its wrapper and its document.
Stylesheet
/* smbk-file-upload — File upload. Figma "File upload" 7110:345 (Type: Drop zone, Button only × State: Default,
* Drag-over, Focus, Disabled, Error).
* A native file input, unseen but in the tab order, inside a label drawn as the secondary button, so the browser's
* own picker always opens: from a press on the button, or Enter or Space on the focused input. The drop zone is a
* dashed area round an upload icon, a title over the line that says what is accepted, the button and, in error,
* the field message; dropping files on it is an extra. Button only is the button over the same line, for narrow
* places. Focus is the input's, drawn as the focus ring round the zone (round the button alone in the button-only
* form); disabled is the input's attribute; error and dragging are flags. While files are held over it the zone
* keeps its height and swaps its body for the drop layer: the upload icon and "Drop to add" over the same line,
* centred, on the sunken surface inside the brand edge. The chosen files are file rows in a list under it.
* The edge is drawn inside, so the paddings give its width back, at the hairline and at the emphasis width. */
.smbk-file-upload {
display: flex;
flex-direction: column;
gap: var(--smbk-space-stack-md);
min-width: 0;
}
/* The block frames the words it hides for screen readers, so they stay inside it wherever it is placed: without a
frame they hang from the page itself and stretch it, past the end of an area that scrolls. No weight, so a parent
that places the block keeps its say. */
:where(.smbk-file-upload) {
position: relative;
}
/* ---- the drop zone ---- */
.smbk-file-upload__zone {
box-sizing: border-box;
display: grid;
min-width: 0;
padding: calc(var(--smbk-space-inset-2xl) - var(--smbk-border-width-hairline));
border: var(--smbk-border-width-hairline) dashed var(--smbk-color-border-strong);
border-radius: var(--smbk-radius-container-md);
background: var(--smbk-color-surface-default);
text-align: center;
}
/* The body and the drop layer share one cell, so the zone is as tall as its body whichever shows. */
.smbk-file-upload__body,
.smbk-file-upload__drop {
grid-area: 1 / 1;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--smbk-space-stack-lg);
min-width: 0;
}
.smbk-file-upload__drop {
justify-content: center;
visibility: hidden;
}
.smbk-file-upload__icon {
font-size: var(--smbk-size-icon-lg);
color: var(--smbk-color-icon-subtle);
}
.smbk-file-upload__text {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--smbk-space-stack-xs);
min-width: 0;
}
.smbk-file-upload__title {
margin: 0;
color: var(--smbk-color-text-default);
font-family: var(--smbk-typography-body-md-medium-font-family);
font-size: var(--smbk-typography-body-md-medium-font-size);
font-weight: var(--smbk-typography-body-md-medium-font-weight);
line-height: var(--smbk-typography-body-md-medium-line-height);
letter-spacing: var(--smbk-typography-body-md-medium-letter-spacing);
}
/* What is accepted: the types, the largest size, how many. */
.smbk-file-upload__constraints {
margin: 0;
color: var(--smbk-color-text-subtle);
font-family: var(--smbk-typography-body-xs-font-family);
font-size: var(--smbk-typography-body-xs-font-size);
font-weight: var(--smbk-typography-body-xs-font-weight);
line-height: var(--smbk-typography-body-xs-line-height);
letter-spacing: var(--smbk-typography-body-xs-letter-spacing);
}
/* The button is a label round the input, carrying the button's classes, so its looks are the button's. */
.smbk-file-upload__button {
position: relative;
max-width: 100%;
}
/* The input stays in the page and the tab order, unseen; the button and the zone draw its focus. */
/* .smbk-file-upload__input: hidden from view, kept for screen readers and the keyboard, by the shared rule in visually-hidden/visually-hidden.css. */
.smbk-file-upload__input {
clip-path: inset(50%);
}
/* The field message across the zone's width, its words from the start edge, as Figma draws it. */
.smbk-file-upload__message {
align-self: stretch;
text-align: start;
}
/* Focus: the ring goes round the zone, not the button inside it. */
.smbk-file-upload--type-drop-zone .smbk-file-upload__zone:has(.smbk-file-upload__input:focus-visible) {
outline: var(--smbk-focus-ring-width) solid var(--smbk-color-border-focus);
outline-offset: var(--smbk-focus-ring-offset);
}
.smbk-file-upload--type-drop-zone .smbk-file-upload__button:has(> .smbk-file-upload__input:focus-visible) {
outline: none;
}
/* Error: the danger edge at the emphasis width; its file input marked aria-invalid="true" draws the same. */
.smbk-file-upload--error .smbk-file-upload__zone,
.smbk-file-upload:where(:has([aria-invalid="true"])) .smbk-file-upload__zone {
padding: calc(var(--smbk-space-inset-2xl) - var(--smbk-border-width-emphasis));
border-width: var(--smbk-border-width-emphasis);
border-color: var(--smbk-color-feedback-danger-border);
}
/* Dragging: the brand edge at the emphasis width and the sunken surface, the drop layer in the body's place.
The button-only form takes no drop, so it has no dragging look. */
.smbk-file-upload--dragging:not(.smbk-file-upload--type-button-only) .smbk-file-upload__zone {
padding: calc(var(--smbk-space-inset-2xl) - var(--smbk-border-width-emphasis));
border-width: var(--smbk-border-width-emphasis);
border-color: var(--smbk-color-border-brand);
background: var(--smbk-color-surface-sunken);
}
.smbk-file-upload--dragging:not(.smbk-file-upload--type-button-only) .smbk-file-upload__body {
visibility: hidden;
}
.smbk-file-upload--dragging:not(.smbk-file-upload--type-button-only) .smbk-file-upload__drop {
visibility: visible;
}
/* Disabled: the icon and the words take the disabled colors; the button takes its own disabled look. */
.smbk-file-upload:has(.smbk-file-upload__input:disabled) .smbk-file-upload__icon {
color: var(--smbk-color-icon-disabled);
}
.smbk-file-upload:has(.smbk-file-upload__input:disabled) :is(.smbk-file-upload__title, .smbk-file-upload__constraints) {
color: var(--smbk-color-text-disabled);
}
/* ---- button only: the button over the line, stacked from the start edge, with no zone drawn ---- */
.smbk-file-upload--type-button-only .smbk-file-upload__zone {
padding: 0;
border: 0;
border-radius: 0;
background: none;
text-align: start;
}
.smbk-file-upload--type-button-only .smbk-file-upload__body {
align-items: flex-start;
gap: var(--smbk-space-inline-md);
}
/* ---- the chosen files ---- */
.smbk-file-upload__files {
display: flex;
flex-direction: column;
gap: var(--smbk-space-stack-md);
margin: 0;
padding: 0;
list-style: none;
}
.smbk-file-upload__file {
min-width: 0;
}
/* What changed, for a screen reader: in the page, not drawn. */
/* .smbk-file-upload__announcement: hidden from view, kept for screen readers and the keyboard, by the shared rule in visually-hidden/visually-hidden.css. */
.smbk-file-upload__announcement {
clip-path: inset(50%);
}
/* ---- high contrast (forced colors) ---- */
/* The dragging zone's sunken surface is dropped in Windows high-contrast mode, so its edge says it: Highlight. */
@media (forced-colors: active) {
.smbk-file-upload--dragging:not(.smbk-file-upload--type-button-only) .smbk-file-upload__zone {
border-color: Highlight;
}
}Tokens it reads
| Token and CSS | Value | Preview | Foundation |
|---|---|---|---|
| space.stack.md--smbk-space-stack-md | comfortable: 12pxcompact: 8pxchanges with density | Spacing | |
| space.inset.2xl--smbk-space-inset-2xl | comfortable: 24pxcompact: 16pxchanges with density | Spacing | |
| border-width.hairline--smbk-border-width-hairline | 1px | Border width | |
| color.border.strong--smbk-color-border-strong | light: #9ba5b1dark: #373d43changes with theme | Color | |
| radius.container.md--smbk-radius-container-md | 12px | Radius | |
| color.surface.default--smbk-color-surface-default | light: #f3f5f8dark: #181a1cchanges with theme | Color | |
| space.stack.lg--smbk-space-stack-lg | comfortable: 16pxcompact: 12pxchanges with density | Spacing | |
| size.icon.lg--smbk-size-icon-lg | 24px | Size | |
| color.icon.subtle--smbk-color-icon-subtle | light: #4e555edark: #c4cdd8changes with theme | Color | |
| space.stack.xs--smbk-space-stack-xs | 4px | Spacing | |
| color.text.default--smbk-color-text-default | light: #232527dark: #e7ebf0changes with theme | Color | |
| typography.body-md-medium--smbk-typography-body-md-medium-font-family--smbk-typography-body-md-medium-font-size--smbk-typography-body-md-medium-font-weight--smbk-typography-body-md-medium-line-height--smbk-typography-body-md-medium-letter-spacing--smbk-typography-body-md-medium-text-transform | fontFamily: Geist, Geist Fallback, system-ui, sans-seriffontSize: 16pxfontWeight: 500lineHeight: 1.5letterSpacing: 0px | The quick brown fox | Typography |
| color.text.subtle--smbk-color-text-subtle | light: #4e555edark: #9ba5b1changes with theme | Color | |
| typography.body-xs--smbk-typography-body-xs-font-family--smbk-typography-body-xs-font-size--smbk-typography-body-xs-font-weight--smbk-typography-body-xs-line-height--smbk-typography-body-xs-letter-spacing--smbk-typography-body-xs-text-transform | fontFamily: Geist, Geist Fallback, system-ui, sans-seriffontSize: 12pxfontWeight: 400lineHeight: 1.5letterSpacing: 0px | The quick brown fox | Typography |
| focus.ring.width--smbk-focus-ring-width | 2px | Focus | |
| color.border.focus--smbk-color-border-focus | light: #cc0d0ddark: #f34336changes with theme | Color | |
| focus.ring.offset--smbk-focus-ring-offset | 2px | Focus | |
| border-width.emphasis--smbk-border-width-emphasis | 2px | Border width | |
| color.feedback.danger.border--smbk-color-feedback-danger-border | #f34336 | Color | |
| color.border.brand--smbk-color-border-brand | #cc0d0d | Color | |
| color.surface.sunken--smbk-color-surface-sunken | light: #373d430ddark: #00000066changes with theme | Color | |
| color.icon.disabled--smbk-color-icon-disabled | light: #9ba5b1dark: #606a75changes with theme | Color | |
| color.text.disabled--smbk-color-text-disabled | light: #7c8692dark: #606a75changes with theme | Color | |
| space.inline.md--smbk-space-inline-md | comfortable: 8pxcompact: 6pxchanges with density | Spacing |
Manifest
{
"name": "file-upload",
"block": "smbk-file-upload",
"summary": "A place to choose files or drop them, with what is accepted said first, and each chosen file in a row below with its progress and result.",
"adapt": "css",
"axes": {
"type": {
"values": [
"drop-zone",
"button-only"
],
"default": "drop-zone",
"description": "A drop zone is a dashed area with a title, what is accepted and a button, that also takes files dropped on it; button only is the button over what is accepted, for narrow places. Figma calls them \"Drop zone\" and \"Button only\"."
}
},
"flags": {
"error": {
"description": "Something is wrong, such as no file chosen when one is required. The zone takes the danger edge; say what is wrong in the message."
},
"dragging": {
"description": "Files are held over the zone: the brand edge, the sunken surface and \"Drop to add\", at the same height. The wrapper sets it while dragging; Figma calls it \"Drag-over\"."
}
},
"parts": [
"zone",
"body",
"icon",
"text",
"title",
"constraints",
"button",
"input",
"message",
"drop",
"files",
"file",
"announcement"
],
"figmaNodeId": "7110:345"
}