One chosen file: its name, size and state, its progress while it uploads, and what can be done with it.
quarterly-report.pdf
Options
| Choice | Values | Default | What it does |
|---|---|---|---|
statussmbk-file-row--status-<value> | selecteduploadingcompletefailedrejected | selected | Selected is chosen and waiting; uploading shows the spinner and the progress bar; complete has gone through; failed did not, and can be tried again; rejected was not added, and says why. |
Examples
Usage
A file going up: how much of it has gone, the turning ring and “Uploading…”, the bar under it, and a button that stops it.
quarterly-report.pdf
Statuses
Chosen and waiting, uploading, uploaded, failed with Try again beside Remove, and refused with the reason.
quarterly-report.pdf
quarterly-report.pdf
quarterly-report.pdf
quarterly-report.pdf
quarterly-report.xlsx
Long name
A long name is cut short with an ellipsis; the extension stays whole.
quarterly-report-final-version-approved-by-the-board.pdf
File types
The icon follows the file’s type: a document, a spreadsheet, slides, a picture, a sound, an archive.
board-minutes.docx
budget-2027.xlsx
launch-deck.pptx
site-photo.jpg
interview.mp3
brand-assets.zip
Narrow
In a narrow place the line keeps the size and the state together; a long reason wraps beside its own icon.
quarterly-report.pdf
quarterly-report.pdf
quarterly-report.pdf
Unknown progress
Without its size or progress the bar shows its unknown state, and the line says only “Uploading…”.
interview.mp3
Guidelines
Purpose
One chosen file, shown as a card: its type, its name, its size and its state, its progress while it uploads, and the buttons for what can be done with it. A file upload draws one for each file chosen; a list of attachments can use it on its own.
When to use
- Each file chosen in a file upload, which draws them for you.
- A list of files already attached, such as the attachments of a message.
When not to use
Usage rules
Do
- Say why a file was not added, in its message: "Not added: it must be a PDF."
- Give only the handlers the page can carry out.
Don't
- Show a file as uploaded before the server has taken it.
- Read out every percent of an upload.
site-photo.heic
Say why a file was not added, so the reader can put it right.
site-photo.heic
A bare “Not added” leaves the reader guessing what to change.
Anatomy
- The block, a card with a subtle edge: the top part, then, while the file uploads, the progress part.
- The top part, a row: the icon part, the text part, then the actions part at the top end.
- The icon part: the file's type, such as a document, a spreadsheet or a picture, picked from its extension or its media type, at the 2xl icon size.
- The text part: the name part over the meta part.
- The name part: the stem part, the name before its extension, then the extension part. A long name is cut short with an ellipsis in the stem, and the extension stays whole.
- The meta part, one line: the size part, the separator part (a dot), then the state part. The size shows while the file is chosen, uploading or uploaded; while it uploads it says how much has gone ("1.1 of 2.4 MB").
- The state part: the state-icon part (the spinner's subtle ring while uploading, a tick once uploaded, a crossed circle when it failed, a warning sign when it was not added) and the message part, the words. A failed or refused file shows only its icon and reason, which wraps beside its icon.
- The actions part: the button's small ghost icon-only form, named with the file: Remove for a file chosen, failed or not added; Cancel while it uploads; Try again then Remove when it failed; Delete once uploaded.
- The progress part: the progress bar's small size with no label, the row's full width, named with the file.
- The status is a modifier:
--status-selected,--status-uploading,--status-complete,--status-failed,--status-rejected.
Behavior
The row is markup and words only: it uploads nothing, and the page sets its status and its progress. Each button is drawn only when its handler is given, so a row offers only what the page can do. While the file uploads, the bar fills from loaded and size, or from progress; without either it shows its unknown state. The spinner's ring turns while the file uploads, and stands still under reduced motion. Sizes are written in English, or in the language set with locale, so a page prepared on a server shows the same sizes once its script runs.
Accessibility
What it does for you
- The file's icon is decoration, hidden from assistive technology: the name says the same.
- Each button is named with the file, such as "Remove quarterly-report.pdf".
- The state is an icon and words, never color alone. A failed or refused file says why in words.
- The progress bar is named with the file.
- The name is written as text, never as markup, and follows its own direction, so a name in a right-to-left script reads the right way.
- A row alone announces nothing. In a file upload, its changes of status are read out for it.
What you must do
- Use
removeLabel,cancelLabel,retryLabelanddeleteLabelto change the buttons' words for another language. - Say why a file was refused, in its message.
- On a page that uses rows on their own, announce their changes yourself, once each, not every percent.
How to test
- With a screen reader, each button is read with the file's name, and the progress bar is read by the file.
- Tab through the row: each button takes focus, and Enter or Space presses it.
- With the colors removed, the icon and words still say whether the file is uploading, done, failed or refused.
- With text at 200% and the page 320 pixels wide, a long name is cut short with an ellipsis, its extension stays whole, and the buttons stay in view.
- In a forced-color mode, the icon and the buttons are still visible.
Keyboard
| Key | What it does |
|---|---|
| Tab | Moves focus to the next button in the row. |
| EnterorSpace | Presses the focused button: remove, cancel, try again or delete. |
Source
The files the file row is made of. Its folder on GitHub (opens in a new tab) holds them with its wrapper and its document.
Stylesheet
/* smbk-file-row — File row. Figma "File row" 7110:252 (Status: Selected, Uploading, Complete, Failed, Rejected).
* One chosen file: a card that stacks a top row over, while it uploads, the progress bar. The top row holds the
* file's icon at the 2xl icon size, then its name over a line with the size, a dot and the state, then its
* actions, the button's small ghost icon-only form, at the top end. The name is two texts, so a long one is cut
* short with an ellipsis and the extension stays whole. The state is an icon (or the spinner's ring) and words,
* so it never rests on color alone; a failed or refused file shows only the reason, which wraps beside its icon.
* The edge is drawn inside, so the paddings give its width back. */
.smbk-file-row {
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: var(--smbk-space-stack-md);
min-width: 0;
padding: calc(var(--smbk-space-inset-lg) - var(--smbk-border-width-hairline));
border: var(--smbk-border-width-hairline) solid var(--smbk-color-border-subtle);
border-radius: var(--smbk-radius-container-sm);
background: var(--smbk-color-surface-default);
color: var(--smbk-color-text-default);
}
.smbk-file-row__top {
display: flex;
align-items: flex-start;
gap: var(--smbk-space-inline-lg);
min-width: 0;
}
/* The file's type, as an icon chosen from its extension: decoration, since the name says the same. */
.smbk-file-row__icon {
flex: none;
font-size: var(--smbk-size-icon-2xl);
color: var(--smbk-color-icon-subtle);
}
.smbk-file-row__text {
display: flex;
flex: 1;
flex-direction: column;
gap: var(--smbk-space-stack-xs);
min-width: 0;
}
.smbk-file-row__name {
display: flex;
min-width: 0;
margin: 0;
color: var(--smbk-color-text-default);
font-family: var(--smbk-typography-body-sm-medium-font-family);
font-size: var(--smbk-typography-body-sm-medium-font-size);
font-weight: var(--smbk-typography-body-sm-medium-font-weight);
line-height: var(--smbk-typography-body-sm-medium-line-height);
letter-spacing: var(--smbk-typography-body-sm-medium-letter-spacing);
}
/* The name before its extension shrinks first, cut short with an ellipsis; the extension keeps its width. */
.smbk-file-row__stem {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.smbk-file-row__extension {
flex: none;
white-space: nowrap;
}
/* The line under the name: the size, a dot, then the state. It never wraps between them; only a long reason
wraps, beside its own icon. */
.smbk-file-row__meta {
display: flex;
align-items: flex-start;
gap: var(--smbk-space-inline-sm);
min-width: 0;
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);
}
.smbk-file-row__size,
.smbk-file-row__separator {
flex: none;
white-space: nowrap;
}
/* Figures of one width, so the size counting up while a file uploads ("1.1 of 2.4 MB") never moves the state after it. */
.smbk-file-row__size {
font-variant-numeric: tabular-nums;
}
.smbk-file-row__state {
display: flex;
flex: 1;
align-items: flex-start;
gap: var(--smbk-space-inline-xs);
min-width: 0;
}
/* The state's icon, and the spinner's ring while uploading, sit in a box one line tall, on the first line's middle. */
.smbk-file-row__state-icon {
flex: none;
height: calc(var(--smbk-typography-body-xs-font-size) * var(--smbk-typography-body-xs-line-height));
font-size: var(--smbk-size-icon-sm);
}
.smbk-file-row__message {
min-width: 0;
}
.smbk-file-row--status-complete .smbk-file-row__message {
color: var(--smbk-color-feedback-success-text-on-subtle);
}
.smbk-file-row--status-complete .smbk-file-row__state-icon {
color: var(--smbk-color-feedback-success-icon);
}
.smbk-file-row--status-failed .smbk-file-row__message,
.smbk-file-row--status-rejected .smbk-file-row__message {
color: var(--smbk-color-feedback-danger-text-on-subtle);
}
.smbk-file-row--status-failed .smbk-file-row__state-icon,
.smbk-file-row--status-rejected .smbk-file-row__state-icon {
color: var(--smbk-color-feedback-danger-icon);
}
.smbk-file-row__actions {
display: flex;
flex: none;
gap: var(--smbk-space-inline-xs);
}
/* While uploading: the progress bar's small size, with no label, the row's full width. */
.smbk-file-row__progress {
width: 100%;
}
/* ---- touch: the actions stop their press areas halfway to each other ----
Every button takes an invisible press area on a touch screen; the row's actions stand closer than that, so between
two of them each area reaches only halfway into their gap. Nothing drawn moves. */
@media (pointer: coarse) {
.smbk-file-row__actions > * + * .smbk-btn::after,
.smbk-file-row__actions > .smbk-btn + .smbk-btn::after,
.smbk-file-row__actions > * + .smbk-btn::after {
inset-inline-start: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), var(--smbk-space-inline-xs) / -2);
}
.smbk-file-row__actions > *:has(+ *) .smbk-btn::after,
.smbk-file-row__actions > .smbk-btn:has(+ *)::after {
inset-inline-end: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), var(--smbk-space-inline-xs) / -2);
}
}Tokens it reads
| Token and CSS | Value | Preview | Foundation |
|---|---|---|---|
| space.stack.md--smbk-space-stack-md | comfortable: 12pxcompact: 8pxchanges with density | Spacing | |
| space.inset.lg--smbk-space-inset-lg | comfortable: 12pxcompact: 8pxchanges with density | Spacing | |
| border-width.hairline--smbk-border-width-hairline | 1px | Border width | |
| color.border.subtle--smbk-color-border-subtle | light: #c4cdd8dark: #2d3237changes with theme | Color | |
| radius.container.sm--smbk-radius-container-sm | 8px | Radius | |
| color.surface.default--smbk-color-surface-default | light: #f3f5f8dark: #181a1cchanges with theme | Color | |
| color.text.default--smbk-color-text-default | light: #232527dark: #e7ebf0changes with theme | Color | |
| space.inline.lg--smbk-space-inline-lg | comfortable: 12pxcompact: 8pxchanges with density | Spacing | |
| size.icon.2xl--smbk-size-icon-2xl | 40px | Size | |
| color.icon.subtle--smbk-color-icon-subtle | light: #4e555edark: #c4cdd8changes with theme | Color | |
| space.stack.xs--smbk-space-stack-xs | 4px | Spacing | |
| typography.body-sm-medium--smbk-typography-body-sm-medium-font-family--smbk-typography-body-sm-medium-font-size--smbk-typography-body-sm-medium-font-weight--smbk-typography-body-sm-medium-line-height--smbk-typography-body-sm-medium-letter-spacing--smbk-typography-body-sm-medium-text-transform | fontFamily: Geist, Geist Fallback, system-ui, sans-seriffontSize: 14pxfontWeight: 500lineHeight: 1.5letterSpacing: 0px | The quick brown fox | Typography |
| space.inline.sm--smbk-space-inline-sm | comfortable: 6pxcompact: 4pxchanges with density | Spacing | |
| 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 |
| space.inline.xs--smbk-space-inline-xs | 4px | Spacing | |
| size.icon.sm--smbk-size-icon-sm | 16px | Size | |
| color.feedback.success.text-on-subtle--smbk-color-feedback-success-text-on-subtle | light: #016317dark: #8ddf90changes with theme | Color | |
| color.feedback.success.icon--smbk-color-feedback-success-icon | light: #037a1fdark: #54b85bchanges with theme | Color | |
| color.feedback.danger.text-on-subtle--smbk-color-feedback-danger-text-on-subtle | light: #a51d17dark: #febdb2changes with theme | Color | |
| color.feedback.danger.icon--smbk-color-feedback-danger-icon | light: #cc0d0ddark: #fe7d6dchanges with theme | Color | |
| size.target.comfortable--smbk-size-target-comfortable | 44px | Size |
Manifest
{
"name": "file-row",
"block": "smbk-file-row",
"summary": "One chosen file: its name, size and state, its progress while it uploads, and what can be done with it.",
"adapt": "css",
"axes": {
"status": {
"values": [
"selected",
"uploading",
"complete",
"failed",
"rejected"
],
"default": "selected",
"description": "Selected is chosen and waiting; uploading shows the spinner and the progress bar; complete has gone through; failed did not, and can be tried again; rejected was not added, and says why."
}
},
"flags": {},
"parts": [
"top",
"icon",
"text",
"name",
"stem",
"extension",
"meta",
"size",
"separator",
"state",
"state-icon",
"message",
"actions",
"progress"
],
"figmaNodeId": "7110:252"
}