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
  • Accessibility
  • Figma Progress Bar 281:9029
  • packages/ui/src/components/Progress

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.

Course completion
6 of 10 lessons finished
tsx
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

bash
pnpm add @nuton/ui @nuton/tokens

Peer dependencies: react and react-dom 18 or later. Progress needs no icons.

2. Load tokens and component styles once

tsx
// 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

tsx
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`}
    />
  )
}
  • value is required and runs from 0 to max (default 100). Values outside the range are clamped, so you can pass raw counts.
  • Every progress bar needs a name: label on Progress, or aria-label / aria-labelledby. CircularProgress has no label prop, 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 formatValue for 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 loading state.

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

Blue
Red
Orange
Green
Not started
colorFill tokenUse for
blue--nu-state-primary-baseThe default for any task in progress.
green--nu-state-success-baseA finished task or a goal reached.
orange--nu-state-warning-baseNearly at a limit, such as space storage at 90%.
red--nu-state-error-baseOver a limit or a failed task.
empty--nu-bg-soft-200The 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

Weekly goal
Weekly goal

With 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

Space storage
Manage storageUpgrade to keep adding lesson videos.

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

Cell biology
Next: Mitosis and meiosis

Ring sizes

80 · 6.4px ring
72 · 5.76px ring
64 · 5.12px ring
56 · 4.48px ring
48 · 6.72px ring

Dimensions

PartSizeSpacingType
Bar6px tall, full width, full radius——
Label on top32px (title row 20 + 6 + bar 6); 54px with the bottom row6px between rows; 6px in the title row; 4px in the bottom rowTitle Label/Small (Strong-950); value and description Paragraph/X Small (Sub-600)
Label on right16px row8px between bar and valueValue Paragraph/X Small (Sub-600)
Ring 80 / 72 / 6480 / 72 / 64pxRing 6.4 / 5.76 / 5.12pxNumber Label/Small
Ring 5656pxRing 4.48pxNumber Label/X Small
Ring 4848pxRing 6.72pxNo 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.

Not started
Not startedAt 0 the fill isn't drawn; only the track shows.
In progress
In progressAny value between 0 and max. Figma shows steps of 10; any number works.
Complete
CompleteAt max. Switch to green and say so in words.
Generating flashcards
UpdatingPress Start. The fill eases between values; the transition is off under reduced motion.
Ring valuesThe same states on the ring. Figma shows steps of 25.
Not built
IndeterminateFigma has no indeterminate variant, so Progress always needs a value. An unknown-length wait needs a design decision (C41).

Tokens

The colour prop sets these custom properties. Override them on a wrapper only for a documented exception.

TokenDefaultSet by
--nu-progress-fillState/Primary/Basecolor (both forms)
--nu-progress-trackBg/Soft-200Fixed
--nu-progress-height6pxBar only
--nu-circular-progress-size80pxsize

Composition examples

Course tile

The ring sits beside a visible heading, which names it through aria-labelledby, so the name isn't repeated.

Organic chemistry7 of 11 lessons · Next: Alkenes

Lesson list

Use labelPosition="right" in dense rows. The visually hidden label still names each bar.

  • Photosynthesis
    Photosynthesis
  • Cellular respiration
    Cellular respiration
  • The nitrogen cycle
    The nitrogen cycle

Multi-step setup

For a short sequence, count steps with max and formatValue. The description names the current step.

Set up your learning space
Choose your subjects

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

Progress props
PropTypeDefaultDescription
valuerequirednumber—Current value, from 0 to max. Values outside the range, and non-finite values, are clamped.
maxnumber100Maximum 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) => stringlocalised whole %Formats the visible value and aria-valuetext. The default uses Intl.NumberFormat, for example “80%”.
labelReactNode—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).
showValuebooleantrueShows the value text (Figma Show Percentage).
descriptionReactNode—Bottom text, linked with aria-describedby. Top only.
actionReactNode—Bottom action before the description, for example a LinkButton tone="primary" size="sm". Top only.
aria-labelstring—Names the bar when there's no label.
aria-labelledbystring—Names the bar from other elements. Wins over label.
aria-describedbystring—Added after the bottom row in the bar’s description.
refRef<HTMLDivElement>—Forwarded to the root element (the wrapper, not the progressbar).
…restOmit<HTMLAttributes<HTMLDivElement>, 'color' | 'children'>—Any other attribute, passed to the root (className, id, data-*…).

CircularProgress props

CircularProgress props
PropTypeDefaultDescription
valuerequirednumber—Current value, from 0 to max. Clamped like Progress.
maxnumber100Maximum value.
color'blue' | 'red' | 'orange' | 'green' | 'empty''blue'Fill colour (Figma Color).
formatValue(value: number, max: number) => stringlocalised whole %Formats the centre number and aria-valuetext.
size80 | 72 | 64 | 56 | 4880Diameter in px (Figma Size).
showValuebooleantrueShows the number in the centre (Figma Number). Never shown at size 48.
aria-labelstring—Names the ring. Needed unless you pass aria-labelledby (development warning otherwise).
aria-labelledbystring—Names the ring from a visible heading.
refRef<HTMLDivElement>—Forwarded to the root, which is the progressbar.
…restOmit<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

KeyBehaviour
TabSkips 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 · propertyFigma valuesCode
Progress Bar · Percentage0–100% in steps of 10value, max (any number)
Progress Bar Line · ColorBlue · Red · Orange · Green · Emptycolor
Progress Bar Label · TypeOn Top · On RightlabelPosition: top · right
Progress Bar Label · Edit TitleTextlabel
Progress Bar Label · Show PercentageOff · OnshowValue (text via formatValue)
Progress Bar Label · Show Bottom, Edit DescriptionOff · On, textdescription
Progress Bar Label · Link ButtonInstanceaction
Circular Progress Bar · Size80 · 72 · 64 · 56 · 48size
Circular Progress Bar · Percentage0 · 25 · 50 · 75 · 100value (any number)
Circular Progress Bar · NumberOff · 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 loading state 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

Introduction to biology
6 of 10 lessons finished
DoSay what the bar measures. A title and a count tell students exactly where they stand.
Don'tDon't show a bare bar. Without a title, nobody knows what 60% refers to, and screen readers get no name.
Lab safety quiz
Complete
DoUse colour to support the words. Green plus “Complete” works for everyone, including people who can't tell green from blue.
Lab safety quiz
Don'tDon't rely on colour alone. Red with no explanation doesn't say what went wrong or what to do.
DoPick the ring size by context. 64 to 80 for a course tile; 48 in tight lists, where the number is dropped.
Don'tDon't use the ring for long, thin layouts. In a list row a bar reads faster and lines up with its neighbours.

Common mistakes

  • Leaving CircularProgress without aria-label or aria-labelledby. It has no label prop.
  • 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 formatValue that returns only a number (“3”). It's also the screen reader text, so include the unit.