Progress
Progress shows how far a task has got, as a bar or a ring. Use it for uploads, course completion and multi-step tasks.
Beta until design decides on an indeterminate state and on the track's 1.19:1 contrast (C41). Everything else on this page is implemented and tested.
Preview
Change the controls to try every supported combination of Progress and CircularProgress. The preview renders the real component and the code updates with it.
import { Progress } from '@nuton/ui'
<Progress
value={60}
label="Course completion"
description="6 of 10 lessons finished"
/>Installation
Availability
@nuton/ui and @nuton/tokens are private workspace packages. They aren't published to a registry yet (D4: registry deferred; D12: distribution waits on the provenance review). Inside the Nuton monorepo, add them as workspace dependencies. The commands below apply once the packages are published.
1. Add the packages
pnpm add @nuton/ui @nuton/tokensnpm install @nuton/ui @nuton/tokensyarn add @nuton/ui @nuton/tokensPeer dependencies: react and react-dom 18 or later. Progress needs no icons.
2. Load tokens and component styles once
// app/layout.tsx (or your app's entry file)
import '@nuton/tokens/tokens.css' // --nu-* design tokens
import '@nuton/ui/styles.css' // component styles (uses the tokens)The title, value and description use --nu-font-body (Inter). Map the token to your loaded font, as described on the Button page.
Usage
import { Progress } from '@nuton/ui'
export function CourseProgress({ done, total }: { done: number; total: number }) {
return (
<Progress
value={done}
max={total}
label="Introduction to biology"
description={`${done} of ${total} lessons finished`}
/>
)
}valueis required and runs from 0 tomax(default 100). Values outside the range are clamped, so you can pass raw counts.- Every progress bar needs a name:
labelon Progress, oraria-label/aria-labelledby. CircularProgress has nolabelprop, so it always needs an ARIA name. - The visible value defaults to a whole percentage, formatted in English so server and browser render the same text. Use
formatValuefor counts such as “3 of 8 lessons”; it also sets what screen readers hear. - Use the bar inside cards, lists and forms; use the ring where space is square, such as a course tile or a dashboard stat.
- Progress needs a known value. There's no indeterminate mode yet (C41): for a wait with no known end, use a Button's
loadingstate.
Variants and sizes
Progress is the bar and CircularProgress is the ring. Both share value, max, color and formatValue. The bar adds a title row and an optional bottom row; the ring adds five sizes.
Colours
<Progress value={60} label="Blue" /> // default
<Progress value={60} color="red" label="Red" />
<Progress value={60} color="orange" label="Orange" />
<Progress value={60} color="green" label="Green" />
<Progress value={0} color="empty" label="Not started" /> // fill matches the trackcolor | Fill token | Use for |
|---|---|---|
blue | --nu-state-primary-base | The default for any task in progress. |
green | --nu-state-success-base | A finished task or a goal reached. |
orange | --nu-state-warning-base | Nearly at a limit, such as space storage at 90%. |
red | --nu-state-error-base | Over a limit or a failed task. |
empty | --nu-bg-soft-200 | The fill is the track colour, so nothing shows. Figma's placeholder for “not started”. |
The track is always --nu-bg-soft-200. Pair a colour with words in the title or description; colour alone doesn't say what changed.
Label position
<Progress value={40} label="Weekly goal" /> // top (default)
<Progress value={40} label="Weekly goal" labelPosition="right" /> // title visually hiddenWith labelPosition="right", Figma shows no title, so label is rendered visually hidden: it still names the bar for screen readers. The bottom row isn't shown in this layout (C41).
Description and action
The bottom row is linked to the bar with aria-describedby. The action sits before the description, as in Figma. To show a value such as “7.2 GB of 10 GB” instead of a percentage, see Value format.
Value format and maximum
<Progress
value={3}
max={8}
label="Cell biology"
formatValue={(value, max) => `${value} of ${max} lessons`}
description="Next: Mitosis and meiosis"
/>
<CircularProgress
value={18}
max={24}
size={72}
color="green"
formatValue={(value, max) => `${value}/${max}`}
aria-label="Quiz score"
/>Ring sizes
80 · 6.4px ring72 · 5.76px ring64 · 5.12px ring56 · 4.48px ring48 · 6.72px ring<CircularProgress value={75} aria-label="Course completion" /> // 80, default
<CircularProgress value={75} size={72} aria-label="Course completion" />
<CircularProgress value={75} size={64} aria-label="Course completion" />
<CircularProgress value={75} size={56} aria-label="Course completion" />
<CircularProgress value={75} size={48} aria-label="Course completion" /> // no numberDimensions
| Part | Size | Spacing | Type |
|---|---|---|---|
| Bar | 6px tall, full width, full radius | — | — |
| Label on top | 32px (title row 20 + 6 + bar 6); 54px with the bottom row | 6px between rows; 6px in the title row; 4px in the bottom row | Title Label/Small (Strong-950); value and description Paragraph/X Small (Sub-600) |
| Label on right | 16px row | 8px between bar and value | Value Paragraph/X Small (Sub-600) |
| Ring 80 / 72 / 64 | 80 / 72 / 64px | Ring 6.4 / 5.76 / 5.12px | Number Label/Small |
| Ring 56 | 56px | Ring 4.48px | Number Label/X Small |
| Ring 48 | 48px | Ring 6.72px | No number |
Taken from the component CSS, which follows Figma's geometry. Ring thickness comes from Figma's inner radius: 0.84 of the radius for sizes 80 to 56, and 0.72 at 48. The ring starts at the top and fills clockwise, with square ends.
States and interactions
Progress isn't interactive, so it has no hover, focus or disabled state. Its states are the values it shows.
max. Figma shows steps of 10; any number works.max. Switch to green and say so in words.Tokens
The colour prop sets these custom properties. Override them on a wrapper only for a documented exception.
| Token | Default | Set by |
|---|---|---|
--nu-progress-fill | State/Primary/Base | color (both forms) |
--nu-progress-track | Bg/Soft-200 | Fixed |
--nu-progress-height | 6px | Bar only |
--nu-circular-progress-size | 80px | size |
Composition examples
Course tile
The ring sits beside a visible heading, which names it through aria-labelledby, so the name isn't repeated.
Lesson list
Use labelPosition="right" in dense rows. The visually hidden label still names each bar.
Multi-step setup
For a short sequence, count steps with max and formatValue. The description names the current step.
File uploads
Each File upload item already contains a Progress bar, named by the file name. Don't add another one beside it.
API reference
Progress props
| Prop | Type | Default | Description |
|---|---|---|---|
valuerequired | number | — | Current value, from 0 to max. Values outside the range, and non-finite values, are clamped. |
max | number | 100 | Maximum value. A max of 0 or less falls back to 100. |
color | 'blue' | 'red' | 'orange' | 'green' | 'empty' | 'blue' | Fill colour (Figma Color). empty draws the fill in the track colour. |
formatValue | (value: number, max: number) => string | localised whole % | Formats the visible value and aria-valuetext. The default uses Intl.NumberFormat, for example “80%”. |
label | ReactNode | — | Title (Figma Edit Title) and the bar's accessible name. Visually hidden with labelPosition="right". |
labelPosition | 'top' | 'right' | 'top' | top: title row above the bar. right: value beside the bar, no title or bottom row (Figma Type). |
showValue | boolean | true | Shows the value text (Figma Show Percentage). |
description | ReactNode | — | Bottom text, linked with aria-describedby. Top only. |
action | ReactNode | — | Bottom action before the description, for example a LinkButton tone="primary" size="sm". Top only. |
aria-label | string | — | Names the bar when there's no label. |
aria-labelledby | string | — | Names the bar from other elements. Wins over label. |
aria-describedby | string | — | Added after the bottom row in the bar’s description. |
ref | Ref<HTMLDivElement> | — | Forwarded to the root element (the wrapper, not the progressbar). |
…rest | Omit<HTMLAttributes<HTMLDivElement>, 'color' | 'children'> | — | Any other attribute, passed to the root (className, id, data-*…). |
CircularProgress props
| Prop | Type | Default | Description |
|---|---|---|---|
valuerequired | number | — | Current value, from 0 to max. Clamped like Progress. |
max | number | 100 | Maximum value. |
color | 'blue' | 'red' | 'orange' | 'green' | 'empty' | 'blue' | Fill colour (Figma Color). |
formatValue | (value: number, max: number) => string | localised whole % | Formats the centre number and aria-valuetext. |
size | 80 | 72 | 64 | 56 | 48 | 80 | Diameter in px (Figma Size). |
showValue | boolean | true | Shows the number in the centre (Figma Number). Never shown at size 48. |
aria-label | string | — | Names the ring. Needed unless you pass aria-labelledby (development warning otherwise). |
aria-labelledby | string | — | Names the ring from a visible heading. |
ref | Ref<HTMLDivElement> | — | Forwarded to the root, which is the progressbar. |
…rest | Omit<HTMLAttributes<HTMLDivElement>, 'color' | 'children'> | — | Any other attribute, passed to the root. |
Styling hooks
Progress renders .nu-progress with data-label-position and data-color; the bar is .nu-progress__track. CircularProgress renders .nu-circular-progress with data-size and data-color. Use props rather than styling these. The tokens in Tokens are the supported override points.
Accessibility
Keyboard
| Key | Behaviour |
|---|---|
| Tab | Skips the progress bar: it isn't interactive and never takes focus. A link in action is a normal tab stop. |
Screen readers
Role progressbar with aria-valuenow, aria-valuemin, aria-valuemax and aria-valuetext. The visible value is hidden so it isn't read twice. The description is linked. Value changes aren't announced on their own; announce milestones such as “Upload complete” in a status message.
Focus
Progress never takes focus, so it has no focus style. Don't add tabIndex to make it readable; screen readers reach it in browse mode.
Contrast and colour
The track (Soft-200) is 1.19:1 against white, below WCAG 1.4.11's 3:1 (C41). Blue is 5.59:1 against white and 4.69:1 against the track. Green is 2.85:1 (the C23 pair); orange 3.44:1 and red 4.15:1 pass 3:1. The value text carries the meaning, so colour is never the only cue. Forced-colours mode uses system colours.
Touch and pointer
Nothing to press. An action link follows Link button's target guidance: a small link is 16px tall, below the 24 × 24 minimum unless it has space around it (C28).
You must: name every bar (label, aria-label or aria-labelledby); say in words when a task completes or fails; keep formatValue text meaningful on its own.
Figma mapping
| Figma component · property | Figma values | Code |
|---|---|---|
| Progress Bar · Percentage | 0–100% in steps of 10 | value, max (any number) |
| Progress Bar Line · Color | Blue · Red · Orange · Green · Empty | color |
| Progress Bar Label · Type | On Top · On Right | labelPosition: top · right |
| Progress Bar Label · Edit Title | Text | label |
| Progress Bar Label · Show Percentage | Off · On | showValue (text via formatValue) |
| Progress Bar Label · Show Bottom, Edit Description | Off · On, text | description |
| Progress Bar Label · Link Button | Instance | action |
| Circular Progress Bar · Size | 80 · 72 · 64 · 56 · 48 | size |
| Circular Progress Bar · Percentage | 0 · 25 · 50 · 75 · 100 | value (any number) |
| Circular Progress Bar · Number | Off · On (none at 48) | showValue |
The value and description bind a remote Text/Sub-600; code uses the local --nu-text-sub-600, which has the same value.
Guidelines and related components
Use Progress for
- Tasks with a known end: uploads, generating flashcards, importing a reading list.
- Completion over time: lessons finished in a course, a weekly study goal.
- Usage against a limit, such as a space's storage.
Don't use Progress for
- Waits with no known end. Use a Button's
loadingstate until an indeterminate design exists (C41). - Scores and ratings that aren't progress, unless the ring's “filled up” meaning fits.
- Choosing a value. Progress is read-only; it isn't a slider.
Do and don't
Common mistakes
- Leaving CircularProgress without
aria-labeloraria-labelledby. It has nolabelprop. - Faking an indeterminate state by looping the value from 0 to 100. Screen readers announce a value that means nothing.
- Passing a description or action with
labelPosition="right". They aren't shown in that layout. - Writing a
formatValuethat returns only a number (“3”). It's also the screen reader text, so include the unit.