Askr
UI & Components

Progress

Use progress with the current published Askr packages.

  • @askrjs/ui/progress0.0.13
  • @askrjs/ui/progress-circle0.0.13
  • @askrjs/themes/progress0.0.13

How to use progress

Progress should use the standard themed surface unless the application is deliberately implementing its own design system on top of @askrjs/ui.

  1. Import the themed component from @askrjs/themes/components; use @askrjs/ui directly only when building a custom visual system.
  2. Keep progress labels, state, and application actions visible at the call site instead of styling internal DOM nodes.
  3. Verify keyboard behavior, focus movement or return, disabled state, and both standard themes.
import { Progress } from '@askrjs/themes/components';

<Progress value={uploaded()} max={total()} aria-label="Upload progress" />

Purpose

Progress and ProgressCircle communicate how far along a task with a known endpoint is — a linear bar or a ring, picked by layout rather than meaning. Neither is interactive: they render a value, not a control, so if what you actually have is an unknown-duration wait, use Spinner instead of forcing a fake percentage into either of these.

Install and import

The headless primitives live at `@askrjs/ui/progress` (linear) and `@askrjs/ui/progress-circle` (circular). The themed layer only ships a dedicated `@askrjs/themes/progress` subpath for the linear bar — ProgressCircle is still themed and re-exported, but you pull it from the full `@askrjs/themes/components` barrel rather than its own subpath.

Live examples

Run the linear and circular variants side by side on this page and watch how `value` changes drive the fill width or the ring's stroke offset in real time. That's also the fastest way to see what an indeterminate `value={null}` state actually looks like before you commit to it in your own UI.

Anatomy

Progress composes a Progress root, which owns `value` and `max`, around a ProgressIndicator that renders the filled portion; ProgressCircle mirrors that exact shape with ProgressCircle and ProgressCircleIndicator. Both indicator components accept `asChild` if you want to render your own element in the indicator's place instead of the default div.

State model

There's no internal state here — `value` is a plain number (or `null` for an indeterminate look) that you own and update on every render, and `max` sets whatever total makes sense for your unit. `getValueLabel(value, max)` turns that pair into the text announced to assistive tech, so use it when the raw percentage on its own wouldn't mean anything to a user, like "3 of 5 files uploaded".

Keyboard and accessibility

Both variants render `role="progressbar"` with `aria-valuenow`, `aria-valuemin`, and `aria-valuemax` kept in sync with `value`/`max`, per PROGRESS_A11Y_CONTRACT. Neither is focusable, so there's no keyboard behavior to test — the accessibility work on your end is almost entirely about supplying a meaningful `getValueLabel`.

Styling and tokens

`data-state` and `data-percentage` land on both the root and the indicator, which is enough for CSS to drive fill width or `stroke-dashoffset` without reading JavaScript state. ProgressCircle's indicator additionally carries a `data-progress-circle-indicator` marker so its styles don't collide with the linear indicator's selectors when both appear in the same stylesheet.

API

Progress and ProgressCircle share the same own-props shape: `value?: number | null`, `max?: number`, and `getValueLabel?: (value, max) => string`, layered on top of whatever `div` props you pass through. ProgressIndicator and ProgressCircleIndicator are purely visual and take no props beyond `asChild`.

Edge cases

`value={null}` is the documented way to signal an indeterminate state, and it should look visually distinct from a real `0` — a user reading "0%" and "unknown yet" as the same thing is a bug, not a styling detail. If you skip `getValueLabel`, the default announcement degrades to a raw percentage, which is fine for a file upload but not for something like disk usage where the number alone doesn't explain what's happening.

See Toast and Sonner for time-boxed feedback instead of a persistent indicator, and Alert, Badge, Empty, Skeleton, and Spinner for the indeterminate-loading counterpart that pairs naturally with Progress.