Progress
Progress: anatomy, keyboard behavior, state, and theming in Askr.
Example
import { Progress } from '@askrjs/themes/components';
<Progress value={uploaded()} max={total()} aria-label="Upload progress" />Published props
Generated from the TypeScript declarations shipped by the installed package. Named types in the Type column define the accepted values.
ProgressIndicatorAsChildProps
Import from @askrjs/ui/progress.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild: true; | — | — |
children | children: JSXElement; | — | — |
ref | ref?: Ref<Element>; | — | — |
ProgressIndicatorProps
Import from @askrjs/ui/progress.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild?: false | undefined; | — | — |
children | children?: unknown; | — | — |
ref | ref?: Ref<HTMLDivElement>; | — | — |
ProgressOwnProps
Import from @askrjs/ui/progress.
| Prop | Type | Default | Description |
|---|---|---|---|
children | children?: unknown; | — | — |
getValueLabel | getValueLabel?: ((value: number | null, max: number) => string) | undefined; | — | — |
id | id?: string | undefined; | — | — |
max | max?: number | undefined; | — | — |
value | value?: number | null | undefined; | — | — |
ProgressProps
Import from @askrjs/ui/progress.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild?: false | undefined; | — | — |
children | children?: unknown; | — | — |
getValueLabel | getValueLabel?: ((value: number | null, max: number) => string) | undefined; | — | — |
id | id?: string | undefined; | — | — |
max | max?: number | undefined; | — | — |
ref | ref?: Ref<HTMLDivElement>; | — | — |
value | value?: number | null | undefined; | — | — |
ProgressCircleIndicatorAsChildProps
Import from @askrjs/ui/progress-circle.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild: true; | — | — |
children | children: JSXElement; | — | — |
ref | ref?: Ref<Element>; | — | — |
ProgressCircleIndicatorProps
Import from @askrjs/ui/progress-circle.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild?: false | undefined; | — | — |
children | children?: unknown; | — | — |
ref | ref?: Ref<HTMLDivElement>; | — | — |
ProgressCircleOwnProps
Import from @askrjs/ui/progress-circle.
| Prop | Type | Default | Description |
|---|---|---|---|
children | children?: unknown; | — | — |
getValueLabel | getValueLabel?: ((value: number | null, max: number) => string) | undefined; | — | — |
id | id?: string | undefined; | — | — |
max | max?: number | undefined; | — | — |
value | value?: number | null | undefined; | — | — |
ProgressCircleProps
Import from @askrjs/ui/progress-circle.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild?: false | undefined; | — | — |
children | children?: unknown; | — | — |
getValueLabel | getValueLabel?: ((value: number | null, max: number) => string) | undefined; | — | — |
id | id?: string | undefined; | — | — |
max | max?: number | undefined; | — | — |
ref | ref?: Ref<HTMLDivElement>; | — | — |
value | value?: number | null | undefined; | — | — |
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
The example puts the linear and circular variants side by side: `value` drives the fill width on one and the ring's stroke offset on the other. Render both with a changing `value`, then with `value={null}`, to see how the indeterminate state reads 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.
Related pages
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.