# 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; } ```