# Progress Bar
Shows how far through a process or task something is.
Section: components
Docs: https://nebula.kaluza.com/components/progress-bar
---
## 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](/components/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
1. **Track**: the full-width background showing the total extent of progress
2. **Fill**: the portion representing progress made so far
3. **Steps**: individual step segments in the `stepped` variant, one per step, filled up to the current step
## Basic usage
```tsx
```
## Variants
### Flat (default)
Use to show progress toward a known percentage. `percentage` is clamped between 0 and 100.
```tsx
```
### 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.
```tsx
```
### Indeterminate
Use when a task is running but you cannot calculate a percentage or step count for it.
```tsx
```
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
- [Progress Stepper](/components/progress-stepper)
- [Skeleton Loading](/components/skeleton-loading)
## Rules and guidance
### Usage
- **ProgressBar** MUST receive both currentStep and totalSteps when using the stepped variant. [id: progressbar.stepped-props; testability: auto]
- **ProgressBar** MUST receive percentage when using the flat variant. [id: progressbar.flat-props; testability: auto]
- **ProgressBar** MUST NOT be used as an interactive element to navigate between steps. Rationale: ProgressBar is display-only. Use ProgressStepper for multi-step forms, and explicit back-navigation controls (CTA Button or Text Link) for step navigation. [id: progressbar.no-navigation; testability: manual]
- **ProgressBar** MUST NOT be relied on to communicate progress to screen reader users without additional context. Rationale: ProgressBar has no ARIA role or live region. Announce progress separately, for example through a visually hidden status message. [id: progressbar.no-aria; testability: context]
- **ProgressBar** SHOULD NOT be used directly to show progress through a multi-step form. Rationale: Use 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 component. [id: progressbar.no-form-progress; testability: context]
### Interaction
- **ProgressBar** WILL clamp the percentage value between 0 and 100 for the flat variant. [id: progressbar.percentage-clamped; testability: auto]
- **ProgressBar** WILL remove the indeterminate animation for users who have prefers-reduced-motion enabled. [id: progressbar.reduced-motion; testability: auto]
## Accessibility
### 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](https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html) [2.4.6 Headings and Labels](https://www.w3.org/WAI/WCAG22/Understanding/headings-and-labels.html)
### 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](https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html) [1.3.1 Info and Relationships](https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html)
#### 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](https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html)
#### 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](https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html)
#### 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](https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html)
### 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 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
### ProgressBar
Also accepts all props from `HTMLAttributes`.
- `variant` (type `'stepped' | 'flat' | 'indeterminate'`, optional, default `'flat'`). Defaults to `flat` - only `stepped` needs to be declared explicitly.
- `currentStep` (type `number`, optional). 1-indexed step the user is currently on. Required for the `stepped` variant.
- `totalSteps` (type `number`, optional). Total number of steps. Required for the `stepped` variant.
- `percentage` (type `number`, optional). How far through the task the user is, from 0 to 100; values outside that range are clamped. Required for the `flat` variant.
- `as` (type `ElementType`, optional). Renders the component as a different element or component, e.g. `as="section"`.
- `forwardedAs` (type `ElementType`, optional). 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
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.
### stepped-track
```css
display: flex;
align-items: center;
gap: ${space[1]};
width: 100%;
padding: ${space[1]} 0;
```
### step
```css
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
```css
position: relative;
overflow: hidden;
width: 100%;
height: 0.6rem;
background-color: ${colors.borderMuted};
border-radius: ${radii.standard};
```
### flat-fill
```css
height: 100%;
width: ${$percentage}%;
background-color: ${colors.primary};
border-radius: ${radii.standard};
```
### indeterminate-fill
```css
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;
}
```