On this page
Progress Bar
Shows how far through a process or task something is.
- Overview
- Rules and guidance
- Properties
- Styles
- Accessibility
When to use
- To show progress toward a known percentage, using the flat variant: for example, how much of a multi-part upload is complete
- To show that a task is running but its duration is not known, using the indeterminate variant: for example, while a file finishes uploading
- As the underlying progress indicator inside another component, using the stepped variant: this is how Progress Stepper shows progress through a form
When not to use
- To show progress through a multi-step form - use Progress Stepper instead, which wraps Progress Bar with a step counter, current step name, and next-step label
- As an interactive element - Progress Bar is display-only and cannot be used to navigate between steps or states
Anatomy

- Track: the full-width background showing the total extent of progress
- Fill: the portion representing progress made so far
- Steps: individual step segments in the stepped variant, one per step, filled up to the current step
Basic usage
Edit live code
<ProgressBar percentage={60} />
Variants
Flat (default)
Use to show progress toward a known percentage. percentage is clamped between 0 and 100.
Edit live code
<ProgressBar percentage={70} />
Stepped
Use to show progress through a fixed number of discrete steps. For multi-step forms, use Progress Stepper instead: it adds the step counter, heading, and next-step label.
Edit live code
<ProgressBar variant="stepped" currentStep={2} totalSteps={4} />
Indeterminate
Use when a task is running but you cannot calculate a percentage or step count for it.
Edit live code
<ProgressBar variant="indeterminate" />
The fill sweeps repeatedly from the start of the track to the end and out of view, rather than tracking a fixed amount. The animation is removed for users who have prefers-reduced-motion enabled.
Related
usage
- ProgressBar must receive both currentStep and totalSteps when using the stepped variantCopy rule ID for progressbar.stepped-props
- ProgressBar must receive percentage when using the flat variantCopy rule ID for progressbar.flat-props
- ProgressBar must not be used as an interactive element to navigate between stepsProgressBar is display-only. Use ProgressStepper for multi-step forms, and explicit back-navigation controls (CTA Button or Text Link) for step navigation.Copy rule ID for progressbar.no-navigation
- ProgressBar must not be relied on to communicate progress to screen reader users without additional contextProgressBar has no ARIA role or live region. Announce progress separately, for example through a visually hidden status message.Copy rule ID for progressbar.no-aria
- ProgressBar should not be used directly to show progress through a multi-step formUse ProgressStepper instead, which wraps ProgressBar with a step counter, current step name, and next-step label.Exception: the stepped variant is used internally by another componentCopy rule ID for progressbar.no-form-progress
interaction
- ProgressBar will clamp the percentage value between 0 and 100 for the flat variantCopy rule ID for progressbar.percentage-clamped
- ProgressBar will remove the indeterminate animation for users who have prefers-reduced-motion enabledCopy rule ID for progressbar.reduced-motion
ProgressBar
Also accepts all props from HTMLAttributes.
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
variant | stepped flat indeterminate | 'flat' | No | Defaults to `flat` - only `stepped` needs to be declared explicitly. |
currentStep | number | — | No | 1-indexed step the user is currently on. Required for the `stepped` variant. |
totalSteps | number | — | No | Total number of steps. Required for the `stepped` variant. |
percentage | number | — | No | How far through the task the user is, from 0 to 100; values outside that range are clamped. Required for the `flat` variant. |
as | ElementType | — | No | Renders the component as a different element or component, e.g. `as="section"`. |
forwardedAs | ElementType | — | No | Like `as`, but passed on to the component being wrapped, for when a styled wrapper should keep its own element and change the one beneath it. |
Styles
stepped-track
display: flex;
align-items: center;
gap: ${space[1]};
width: 100%;
padding: ${space[1]} 0;
step
flex: 1 0 0;
min-width: 3.6rem;
max-width: 12rem;
height: 0.6rem;
background-color: ${$filled ? colors.primary : colors.borderMuted};
&:first-child {
border-top-left-radius: ${radii.standard};
border-bottom-left-radius: ${radii.standard};
}
&:last-child {
border-top-right-radius: ${radii.standard};
border-bottom-right-radius: ${radii.standard};
}
track
position: relative;
overflow: hidden;
width: 100%;
height: 0.6rem;
background-color: ${colors.borderMuted};
border-radius: ${radii.standard};
flat-fill
height: 100%;
width: ${$percentage}%;
background-color: ${colors.primary};
border-radius: ${radii.standard};
indeterminate-fill
position: absolute;
top: 0;
left: 25%;
height: 100%;
width: 50%;
background-color: ${colors.primary};
border-radius: ${radii.standard};
animation: ${sweep} 2s linear infinite;
@media (prefers-reduced-motion) {
animation: none;
}
In brief
Progress Bar is drawn with plain div elements. It has no role, name or value of its own, so a screen reader announces nothing unless you pass them.
What Nebula provides
- Any extra props are forwarded to the bar, so role, aria-label, aria-labelledby, aria-valuenow and aria-hidden can be passed straight to it.
- The indeterminate animation stops for users who ask for reduced motion.
Accessible design
Put a visible label next to the bar that says what is progressing
A bar says nothing without a label: "invoice-2024.pdf" tells a user what the 70% belongs to. The label also gives the bar its name, see the development rules below.
WCAG: 1.3.1 Info and Relationships 2.4.6 Headings and Labels
Accessible development
If the bar is the only place progress is shown, give it role="progressbar", a name and its values
Without a role the progress is visual only. Pass aria-valuenow, aria-valuemin and aria-valuemax for a flat bar, and aria-label or aria-labelledby for the name.
WCAG: 4.1.2 Name, Role, Value 1.3.1 Info and Relationships
Name a progress bar with aria-labelledby or aria-label, not with nearby text
A progress bar takes its name only from those attributes. Point aria-labelledby at the visible label rather than copying the text into aria-label.
WCAG: 4.1.2 Name, Role, Value
For the indeterminate bar, leave out every value attribute
A bar with no known value should not report one, so omit aria-valuenow, aria-valuetext, aria-valuemin and aria-valuemax.
WCAG: 4.1.2 Name, Role, Value
If text beside the bar already says the same thing, hide the bar with aria-hidden="true"
A bar that only repeats a step counter adds nothing for a screen reader. Do not give it role="progressbar" instead: that role makes every descendant presentational.
WCAG: 1.3.1 Info and Relationships
Accessibility testing status
Reviewed on 9 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 | keyboard (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.