Askr documentation
UI & Components

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.

PropTypeDefaultDescription
asChildasChild: true;
childrenchildren: JSXElement;
refref?: Ref<Element>;

ProgressIndicatorProps

Import from @askrjs/ui/progress.

PropTypeDefaultDescription
asChildasChild?: false | undefined;
childrenchildren?: unknown;
refref?: Ref<HTMLDivElement>;

ProgressOwnProps

Import from @askrjs/ui/progress.

PropTypeDefaultDescription
childrenchildren?: unknown;
getValueLabelgetValueLabel?: ((value: number | null, max: number) => string) | undefined;
idid?: string | undefined;
maxmax?: number | undefined;
valuevalue?: number | null | undefined;

ProgressProps

Import from @askrjs/ui/progress.

PropTypeDefaultDescription
asChildasChild?: false | undefined;
childrenchildren?: unknown;
getValueLabelgetValueLabel?: ((value: number | null, max: number) => string) | undefined;
idid?: string | undefined;
maxmax?: number | undefined;
refref?: Ref<HTMLDivElement>;
valuevalue?: number | null | undefined;

ProgressCircleIndicatorAsChildProps

Import from @askrjs/ui/progress-circle.

PropTypeDefaultDescription
asChildasChild: true;
childrenchildren: JSXElement;
refref?: Ref<Element>;

ProgressCircleIndicatorProps

Import from @askrjs/ui/progress-circle.

PropTypeDefaultDescription
asChildasChild?: false | undefined;
childrenchildren?: unknown;
refref?: Ref<HTMLDivElement>;

ProgressCircleOwnProps

Import from @askrjs/ui/progress-circle.

PropTypeDefaultDescription
childrenchildren?: unknown;
getValueLabelgetValueLabel?: ((value: number | null, max: number) => string) | undefined;
idid?: string | undefined;
maxmax?: number | undefined;
valuevalue?: number | null | undefined;

ProgressCircleProps

Import from @askrjs/ui/progress-circle.

PropTypeDefaultDescription
asChildasChild?: false | undefined;
childrenchildren?: unknown;
getValueLabelgetValueLabel?: ((value: number | null, max: number) => string) | undefined;
idid?: string | undefined;
maxmax?: number | undefined;
refref?: Ref<HTMLDivElement>;
valuevalue?: 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.

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.