A track with a fill showing how far something has got, with an optional label and value above it; it can say the work failed, or that its end is not known.
Options
| Choice | Values | Default | What it does |
|---|---|---|---|
tonesmbk-progress--tone-<value> | defaultbrandsuccesswarningdanger | brand | What fills the track. A failed bar is always danger. |
sizesmbk-progress--size-<value> | mdsm | md | Small uses the slim track and the smaller label styles, for dense rows and lists. |
typesmbk-progress--type-<value> | defaultpixel | default | Default is one smooth bar. Pixel cuts the track into twenty segments and fills whole ones. |
statesmbk-progress--state-<value> | defaultfailedunknown | default | Failed leads with the danger icon and words and offers to try again. Unknown shows a detached part of the track and no number, for work whose end is not known. |
Examples
Usage
Label, percentage and the track.
Tones
Five fills.
Small
The slim track, smaller text.
Failed
Leads with the danger icon and words, the fill in danger where it stopped, and offers to try again.
Unknown
Without a value: a detached part of the track and no number, for work whose end is not known.
Pixel
Twenty segments, filled whole; known and unknown, in every tone.
Guidelines
Purpose
Shows how far an amount of work has got: an upload, a multi-step form, a quota. The fill grows along a thin track; a label above says what is happening and, optionally, the percentage. It can also say that the work failed, or that it is going on with no known end.
When to use
- Any wait with a known end: uploads, downloads, steps.
- A quota or a capacity, as a static reading.
- A wait whose end is not known, with the unknown state, when the work takes long enough to need a word about it.
When not to use
- A skeleton's job: holding a layout while content loads.
Usage rules
Do
- Give every progress bar a label, even when hidden, so the role has a name.
- Use the small size in dense rows and lists.
- Write a failed label that names what failed, such as "Upload failed".
Don't
- Use the warning tone to mean "nearly full" without saying so in the label. Color must not be the only cue (WCAG 1.4.1 Use of Color).
Say what the warning is about in the label, so color is not the only cue.
A warning color with a neutral label leaves readers who cannot see the color without the warning.
Anatomy
- The block, a column: the header row, then the track.
- The header part holds the label part and the value part, at either end. When the work failed, the label leads with the icon part, a crossed circle, and the retry part, a "Try again" button underlined like a link, takes the value's place.
- The track part, a thin bar in the subtle track color, pressed into the surface, slimmer at the small size.
- The fill part, the colored bar laid over it from the start, as wide as the progress and as tall as the track. When the end is not known, it is a detached part from three tenths of the track to seven.
- In the pixel type, the track and the fill are cut into twenty square segments, and the fill shows in whole ones.
Behavior
The fill's width is data, set by the wrapper from the value. When the value changes, the fill grows or shrinks to it with the state motion, so the change can be followed; under reduced motion it jumps. The pixel type rounds the value to whole segments, each a twentieth. With showValue the label row shows the percentage, or the value in words when aria-valuetext gives them, as the meter does.
Leave the value out and the bar takes the unknown state: it shows no number, and its detached fill stays still. A failed bar keeps its fill where the work stopped, in the danger color whatever its tone, and its "Try again" calls onRetry; without one it offers no retry.
Accessibility
What it does for you
- The track carries the progress-bar role with its minimum, maximum and current value; the label is its accessible name. The component puts the role, the name and
aria-valuetexton the track. - Assistive technology reads the value from the role, so the visible percentage is a convenience, not the source. A value above the maximum is read as the maximum.
- An unknown bar has no current value, which tells assistive technology its end is not known.
- A failed bar says so in words, its label, not by its color alone. Its retry is a real button, at least 24 pixels square to press.
- In development the wrapper warns in the console when a bar has no name.
- In high contrast mode the empty track is outlined and the done part filled with the highlight color.
What you must do
- Give a bar with no visible label an
aria-label, oraria-labelledbypointing at words elsewhere on the page. - Give
aria-valuetextwhen the value reads better in words. - Write a failed label that names what failed, such as "Upload failed".
- Say in the label how a bar stands when its tone means something, such as "nearly full". Color must not be the only cue (WCAG 1.4.1 Use of Color).
How to test
- With a screen reader, it is read as a progress bar by its name, with its value. An unknown bar is read with no value.
- With a failed bar, the label says what failed, and Tab reaches "Try again", which Enter or Space presses.
- With text at 200% and the page 320 pixels wide, the label row stays whole and the bar fits its width.
- In a forced-color mode, the empty track is outlined and the done part is drawn in the highlight color.
Keyboard
| Key | What it does |
|---|---|
| Tab | On a failed bar, moves focus to "Try again". The bar itself takes no focus. |
| EnterorSpace | On "Try again", presses it. |
Source
The files the progress bar is made of. Its folder on GitHub (opens in a new tab) holds them with its wrapper and its document.
Stylesheet
/* smbk-progress — Progress bar. Figma "Progress Bar" 4168:13627 and "ProgressTrack" 4168:13602.
* The track and the fill are the same height on the same line: the empty track in the subtle track
* color, pressed in, and the fill laid over it from the start edge, clipped by its round ends.
* The fill's width is set inline as a percentage by the wrapper; that is data, not a design value. */
.smbk-progress {
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: var(--smbk-space-stack-sm);
width: 100%;
}
.smbk-progress__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--smbk-space-inline-md);
}
.smbk-progress__label {
display: inline-flex;
align-items: center;
gap: var(--smbk-space-inline-xs);
min-width: 0;
color: var(--smbk-color-text-default);
font-family: var(--smbk-typography-body-sm-font-family);
font-size: var(--smbk-typography-body-sm-font-size);
font-weight: var(--smbk-typography-body-sm-font-weight);
line-height: var(--smbk-typography-body-sm-line-height);
letter-spacing: var(--smbk-typography-body-sm-letter-spacing);
}
.smbk-progress__icon {
flex-shrink: 0;
color: var(--smbk-color-feedback-danger-icon);
font-size: var(--smbk-size-icon-sm);
}
.smbk-progress__value {
color: var(--smbk-color-text-subtle);
font-family: var(--smbk-typography-label-md-font-family);
font-size: var(--smbk-typography-label-md-font-size);
font-weight: var(--smbk-typography-label-md-font-weight);
line-height: var(--smbk-typography-label-md-line-height);
letter-spacing: var(--smbk-typography-label-md-letter-spacing);
text-transform: var(--smbk-typography-label-md-text-transform);
font-variant-numeric: tabular-nums;
}
/* Try again: words underlined like a link, in a box at least big enough to press, the small press box shared in
press-box/press-box.css, with its focus ring and its press area on a touch screen. */
.smbk-progress__retry {
min-width: var(--smbk-size-target-min);
min-height: var(--smbk-size-target-min);
margin-block: calc((var(--smbk-typography-label-md-font-size) * var(--smbk-typography-label-md-line-height) - var(--smbk-size-target-min)) / 2);
border-radius: var(--smbk-radius-control-xs);
color: var(--smbk-color-feedback-danger-text-on-subtle);
font-family: var(--smbk-typography-label-md-font-family);
font-size: var(--smbk-typography-label-md-font-size);
font-weight: var(--smbk-typography-label-md-font-weight);
line-height: var(--smbk-typography-label-md-line-height);
letter-spacing: var(--smbk-typography-label-md-letter-spacing);
text-decoration: underline;
}
.smbk-progress__track {
box-sizing: border-box;
position: relative;
display: flex;
overflow: hidden;
width: 100%;
height: var(--smbk-space-inset-xs);
border-radius: var(--smbk-radius-full);
background: var(--smbk-color-surface-track-subtle);
box-shadow: var(--smbk-elevation-inset);
}
.smbk-progress__fill {
position: relative;
display: block;
height: 100%;
border-radius: var(--smbk-radius-full);
background: var(--smbk-color-surface-brand);
}
/* ---- tone ---- */
.smbk-progress--tone-default .smbk-progress__fill { background: var(--smbk-color-surface-inverse); }
.smbk-progress--tone-success .smbk-progress__fill { background: var(--smbk-color-feedback-success-surface); }
.smbk-progress--tone-warning .smbk-progress__fill { background: var(--smbk-color-feedback-warning-surface); }
.smbk-progress--tone-danger .smbk-progress__fill { background: var(--smbk-color-feedback-danger-surface); }
/* ---- state ---- */
/* Failed: the words and the icon in danger, the fill too, whatever the tone. */
.smbk-progress--state-failed .smbk-progress__label { color: var(--smbk-color-feedback-danger-text-on-subtle); }
.smbk-progress--state-failed .smbk-progress__fill { background: var(--smbk-color-feedback-danger-surface); }
/* Unknown: a detached part of the track, from three tenths to seven, plainly not a measured length. */
.smbk-progress--state-unknown .smbk-progress__fill {
width: 40%;
margin-inline-start: 30%;
}
/* ---- small: the slim track, smaller text ---- */
.smbk-progress--size-sm .smbk-progress__track {
height: var(--smbk-space-inset-2xs);
}
.smbk-progress--size-sm .smbk-progress__label {
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-progress--size-sm .smbk-progress__icon {
font-size: var(--smbk-size-icon-xs);
}
.smbk-progress--size-sm .smbk-progress__value,
.smbk-progress--size-sm .smbk-progress__retry {
font-family: var(--smbk-typography-label-sm-font-family);
font-size: var(--smbk-typography-label-sm-font-size);
font-weight: var(--smbk-typography-label-sm-font-weight);
line-height: var(--smbk-typography-label-sm-line-height);
letter-spacing: var(--smbk-typography-label-sm-letter-spacing);
}
.smbk-progress--size-sm .smbk-progress__retry {
margin-block: calc((var(--smbk-typography-label-sm-font-size) * var(--smbk-typography-label-sm-line-height) - var(--smbk-size-target-min)) / 2);
}
/* ---- pixel: the track cut into twenty square segments a hairline-pair apart, the fill showing
through whole ones. A mask over the track does the cutting, so the segments are the same in the
empty track and the fill, and the wrapper sends a value in whole segments (twentieths). The mask's
color is never seen: only its alpha cuts. ---- */
.smbk-progress--type-pixel .smbk-progress__track {
border-radius: 0;
box-shadow: none;
mask-image: linear-gradient(to right, currentcolor calc(100% - var(--smbk-space-2)), transparent 0);
mask-size: calc((100% + var(--smbk-space-2)) / 20) 100%;
mask-repeat: repeat-x;
}
.smbk-progress--type-pixel .smbk-progress__fill {
border-radius: 0;
}
/* ---- motion: when the value changes, the fill grows or shrinks to it with the state purpose,
so the change can be followed. No movement under reduced motion. ---- */
.smbk-progress__fill {
transition: width var(--smbk-motion-state-duration) var(--smbk-motion-state-easing);
}
@media (prefers-reduced-motion: reduce) {
.smbk-progress__fill {
transition: none;
}
}
/* ---- touch: the retry action's invisible press area (press-box/press-box.css) reaches over the bar under it ---- */
@media (pointer: coarse) {
.smbk-progress__retry::after {
z-index: 1;
}
}
/* ---- high contrast (forced colors) ---- */
/* In Windows high-contrast mode the browser drops fills and shadows; this puts the meaning back with the
user's own colors. Nothing here applies outside that mode. */
@media (forced-colors: active) {
/* The empty track outlined with a hairline inside it, the done part filled with Highlight. */
.smbk-progress__track {
outline: var(--smbk-border-width-hairline) solid CanvasText;
outline-offset: calc(-1 * var(--smbk-border-width-hairline));
}
.smbk-progress .smbk-progress__fill {
background: Highlight;
}
}Tokens it reads
| Token and CSS | Value | Preview | Foundation |
|---|---|---|---|
| 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 | |
| size.target.comfortable--smbk-size-target-comfortable | 44px | Size | |
| space.stack.sm--smbk-space-stack-sm | comfortable: 8pxcompact: 6pxchanges with density | Spacing | |
| space.inline.md--smbk-space-inline-md | comfortable: 8pxcompact: 6pxchanges with density | Spacing | |
| space.inline.xs--smbk-space-inline-xs | 4px | Spacing | |
| color.text.default--smbk-color-text-default | light: #232527dark: #e7ebf0changes with theme | Color | |
| typography.body-sm--smbk-typography-body-sm-font-family--smbk-typography-body-sm-font-size--smbk-typography-body-sm-font-weight--smbk-typography-body-sm-line-height--smbk-typography-body-sm-letter-spacing--smbk-typography-body-sm-text-transform | fontFamily: Geist, Geist Fallback, system-ui, sans-seriffontSize: 14pxfontWeight: 400lineHeight: 1.5letterSpacing: 0px | The quick brown fox | Typography |
| color.feedback.danger.icon--smbk-color-feedback-danger-icon | light: #cc0d0ddark: #fe7d6dchanges with theme | Color | |
| size.icon.sm--smbk-size-icon-sm | 16px | Size | |
| color.text.subtle--smbk-color-text-subtle | light: #4e555edark: #9ba5b1changes with theme | Color | |
| typography.label-md--smbk-typography-label-md-font-family--smbk-typography-label-md-font-size--smbk-typography-label-md-font-weight--smbk-typography-label-md-line-height--smbk-typography-label-md-letter-spacing--smbk-typography-label-md-text-transform | fontFamily: Geist, Geist Fallback, system-ui, sans-seriffontSize: 14pxfontWeight: 500lineHeight: 1.4letterSpacing: 0px | The quick brown fox | Typography |
| size.target.min--smbk-size-target-min | 24px | Size | |
| radius.control.xs--smbk-radius-control-xs | 2px | Radius | |
| color.feedback.danger.text-on-subtle--smbk-color-feedback-danger-text-on-subtle | light: #a51d17dark: #febdb2changes with theme | Color | |
| space.inset.xs--smbk-space-inset-xs | 4px | Spacing | |
| radius.full--smbk-radius-full | 9999px | Radius | |
| color.surface.track-subtle--smbk-color-surface-track-subtle | light: #c4cdd8dark: #2d3237changes with theme | Color | |
| elevation.inset--smbk-elevation-inset | light: inset 0px 0px 1px 0px #373d434ddark: inset 0px 0px 1px 0px #00000066 | Elevation | |
| color.surface.brand--smbk-color-surface-brand | #cc0d0d | Color | |
| color.surface.inverse--smbk-color-surface-inverse | light: #2d3237dark: #e7ebf0changes with theme | Color | |
| color.feedback.success.surface--smbk-color-feedback-success-surface | #037a1f | Color | |
| color.feedback.warning.surface--smbk-color-feedback-warning-surface | #b14904 | Color | |
| color.feedback.danger.surface--smbk-color-feedback-danger-surface | #cc0d0d | Color | |
| space.inset.2xs--smbk-space-inset-2xs | 2px | Spacing | |
| 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 |
| size.icon.xs--smbk-size-icon-xs | 12px | Size | |
| typography.label-sm--smbk-typography-label-sm-font-family--smbk-typography-label-sm-font-size--smbk-typography-label-sm-font-weight--smbk-typography-label-sm-line-height--smbk-typography-label-sm-letter-spacing--smbk-typography-label-sm-text-transform | fontFamily: Geist, Geist Fallback, system-ui, sans-seriffontSize: 12pxfontWeight: 500lineHeight: 1.4letterSpacing: 0px | The quick brown fox | Typography |
| space.2--smbk-space-2 | 2px | Spacing | |
| motion.state.duration--smbk-motion-state-duration | 200ms | Motion | |
| motion.state.easing--smbk-motion-state-easing | cubic-bezier(0.65, 0, 0.35, 1) | Motion | |
| border-width.hairline--smbk-border-width-hairline | 1px | Border width |
Manifest
{
"name": "progress",
"block": "smbk-progress",
"summary": "A track with a fill showing how far something has got, with an optional label and value above it; it can say the work failed, or that its end is not known.",
"adapt": "css",
"axes": {
"tone": {
"values": [
"default",
"brand",
"success",
"warning",
"danger"
],
"default": "brand",
"description": "What fills the track. A failed bar is always danger."
},
"size": {
"values": [
"md",
"sm"
],
"default": "md",
"description": "Small uses the slim track and the smaller label styles, for dense rows and lists."
},
"type": {
"values": [
"default",
"pixel"
],
"default": "default",
"description": "Default is one smooth bar. Pixel cuts the track into twenty segments and fills whole ones."
},
"state": {
"values": [
"default",
"failed",
"unknown"
],
"default": "default",
"description": "Failed leads with the danger icon and words and offers to try again. Unknown shows a detached part of the track and no number, for work whose end is not known."
}
},
"flags": {},
"parts": [
"header",
"label",
"icon",
"value",
"retry",
"track",
"fill"
],
"figmaNodeId": "4168:13627"
}