Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

Progress

A horizontal progress bar for measurable, ongoing tasks — determinate or indeterminate, built on Base UI Progress.

Status
stable
Since
0.1.0
Accessibility pattern
ARIA progressbar

Last updated

x

Install

Add Progress from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.

pnpm dlx shadcn@latest add @vegastack/progress

Usage

import { Progress } from "@/components/ui/progress";

<Progress value={60} aria-label="Upload progress" />;
x

Examples

Anatomy

Progress exposes a flat API while composing Base UI's Root → Track → Indicator parts internally:

  • Root — the role="progressbar" element (data-slot="progress"). className applies here.
  • Track — the muted rail (data-slot="progress-track"). Use trackClassName for rail styling.
  • Indicator — the primary fill (data-slot="progress-indicator"). Use indicatorClassName for fill styling; width and progress semantics are managed by Base UI.

Values

value is reported against max (default 100). The indicator width animates between values with a token-driven transition.

x
x
x
x

Sizes

sm (6px), md (8px), and lg (12px) — the track and indicator scale together.

x
x
x

Custom scale

Pass max to track progress on an arbitrary scale — e.g. step 3 of 5. The value is reported to assistive tech as value / max (here, 60%):

<Progress value={3} max={5} size="lg" aria-label="Step 3 of 5" />
x

Indeterminate

Pass value={null} when the duration is unknown. Base UI drops aria-valuenow and sets data-indeterminate; the indicator becomes a 35%-wide segment sweeping the track, not a filled bar.

That distinction is the whole point: Base UI writes no inline width for an indeterminate indicator, so a bar styled only for the determinate case inherits the track's full width and reads as 100% complete — an upload in progress looking finished. Under prefers-reduced-motion the global reset collapses the sweep to its resting frame, a static 35% segment, so it still never reads as complete.

<Progress value={null} aria-label="Loading" />
x

Colored fill

Recolor the fill with indicatorClassName to signal a status tone — the default is bg-primary. Use a semantic token (bg-success, bg-warning, bg-destructive):

<Progress
  value={66}
  indicatorClassName="bg-success"
  aria-label="Success fill"
/>
x
x
x

Custom track

trackClassName styles the rail behind the fill (default bg-muted). Pair it with indicatorClassName to theme both layers — className stays on the accessible progressbar root:

<Progress
  value={50}
  trackClassName="bg-info-subtle"
  indicatorClassName="bg-info"
  aria-label="Custom track"
/>
x

Size × value

size and value are independent axes — every height works at every fill level.

sm
x
x
x
md
x
x
x
lg
x
x
x

Custom root element (render)

Use Base UI's render to swap the root element while keeping the role="progressbar" semantics, the merged data-slot / state data-*, the forwarded ref, and the Track / Indicator children. The replacement element must support progressbar semantics:

<Progress value={60} aria-label="Upload progress" render={<output />} />
x

Playground

Adjust the bar's value and size — the fill sweeps to each new value — then copy the generated JSX.

x
<Progress value={50} aria-label="Upload progress" />

API Reference

PropTypeDefaultDescription
indicatorClassNamestringClasses for the fill indicator. Use this to change the fill token or motion treatment without replacing the accessible progress root.
maxnumber100Upper bound of the scale — value is reported as value / max.
renderComponentRenderFn<HTMLProps, ProgressRootState> | React.ReactElement<unknown, string | React.JSXElementConstructor<any>>Replace the rendered root element via Base UI render composition. Pass a ReactElement or a render function — Base UI merges this wrapper's data-slot and state data-* onto your element, forwards the ref, and keeps the <Progress.Track> / <Progress.Indicator> children. The element must support role="progressbar" semantics.
sizeProgressSize'md'Track + indicator height. sm (6px), md (8px), lg (12px).
trackClassNamestringClasses for the inner track rail. Use this for track width/height/color overrides; className belongs to the root progressbar element.
valuenumbernullCurrent completion value, between 0 and max. Pass null for an indeterminate bar (Base UI sets data-indeterminate and drops aria-valuenow).

Data attributes and CSS variables on Progress

AttributeValues
data-sizemirrors a prop or state value
data-slot"progress" | "progress-indicator" | "progress-track"

Accessibility

  • Renders role="progressbar" with aria-valuenow / aria-valuemin / aria-valuemax managed by Base UI.
  • When value is null the bar is indeterminate — aria-valuenow is omitted and data-indeterminate is set.
  • Always give the bar an accessible name — an aria-label, or associate a visible label via aria-labelledby.
  • The fill transition and the indeterminate sweep both stop under prefers-reduced-motion through the global reset in base.css — no component restates it.
AttributePurpose
role="progressbar"Exposes the element as a progress bar to assistive tech
aria-valuenowCurrent value (omitted while indeterminate)
aria-valuemin / aria-valuemaxBounds of the scale (0 and max)
aria-label / aria-labelledbyAccessible name (author-provided)
ContractStates tested
Behaviourdefault, indeterminate
Accessibilitylabeled
Visualdefault

Do / Don't

Do
Use Progress for a task with a start and an end — uploads, multi-step forms, onboarding completion — and pass value={null} when the duration is genuinely unknown.
Don't
Use Progress for a wait with no notion of completion at all — a Spinner says that better.

On this page