Progress

A semantic role="progressbar" bar that supports both bounded progress (value + max) and indeterminate mode (a wrapping sub-strip when the endpoint isn't known yet). shadcn-style: pure DOM, forwardRef, themeable via cb-* tokens.

Customize
Value
42
Max
100
Mode

Installation

npx shadcn@latest add https://craftbits.dev/r/progress.json

Usage

import { Progress } from "@craft-bits/core";
 
<Progress value={42} />

With a custom max:

<Progress value={3} max={5} />

Indeterminate mode (omit value or pass indeterminate):

<Progress indeterminate />

Understanding the component

  1. Transform-driven indicator. The fill is a span sized at 100% of the track, translated horizontally by translateX(-${100 - percent}%). Only transform animates, so the indicator rides on the compositor — no layout thrash, no width-animation pitfalls.
  2. Indeterminate via CSS keyframes. When value is undefined (or indeterminate is true), a sub-strip (~40% of the track) slides from -100% to 250% on a 1.4s loop. The @keyframes rule is wrapped in @media (prefers-reduced-motion: no-preference) so reduced-motion users see a static tone-tinted track — no JS branching.
  3. Style injection is idempotent. The indeterminate @keyframes rule is appended to <head> once on first indeterminate mount, guarded by a sentinel <style id="cb-progress-indeterminate-keyframes">. Subsequent mounts skip injection.
  4. Hydration-safe initial paint. The first render holds the indicator at 0% (matching SSR), then animates to the real percent post-mount — so translateX values don't mismatch between server and client.

Props

PropTypeDefaultDescription
valuenumber | nullundefinedCompletion value in [0, max]. Leave undefined for indeterminate mode.
maxnumber100Maximum value. Percentage = value / max * 100.
indeterminatebooleanvalue == nullExplicit override for indeterminate mode.
classNamestringMerged onto the rendered <div> via cn().

Accessibility

  • Renders with role="progressbar" and aria-valuemin={0} / aria-valuemax={max}. Determinate bars additionally expose aria-valuenow rounded to an integer. Indeterminate bars omit aria-valuenow per the ARIA spec.
  • The inner indicator is marked aria-hidden="true" so assistive tech reads only the bar's accessible name — provide aria-label or aria-labelledby for context.
  • The indeterminate animation is automatically suppressed when prefers-reduced-motion: reduce is set — the keyframes are scoped under @media (prefers-reduced-motion: no-preference). The determinate transition is short (300ms) and ends in a static state, so reduced-motion users still see the final position.

Credits

  • Extracted from: AlgoFlashcards (src/platform/ui/progress.tsx), originally a shadcn/Radix <Progress> wrapper. Re-architected as pure DOM (no Radix dep) with max and indeterminate support added.