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/themes/progress.

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

ProgressIndicatorProps

Import from @askrjs/themes/progress.

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

ProgressOwnProps

Import from @askrjs/themes/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/themes/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

Provide an accessible name for each progress indicator. The rendered progressbar exposes aria-valuenow, aria-valuemin and aria-valuemax according to value and max; verify those attributes and the value label for your application.

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 Toaster 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.