Progress Dots
A compact row of step-status dots — one per step. Each dot reflects one of four states (pending, current, correct, wrong). Pass onSelect to make the row tappable; pass results for per-step correct / wrong fills.
Customize
Steps
5
2
sm
Behaviour
Installation
npx shadcn@latest add https://craftbits.dev/r/progress-dots.jsonUsage
import { ProgressDots } from "@craft-bits/core";
<ProgressDots total={5} defaultCurrent={2} />Controlled flow — pair current with onSelect for a tappable stepper:
const [step, setStep] = useState(0);
<ProgressDots
total={5}
current={step}
onSelect={setStep}
results={[true, true, false]}
/>Drop onSelect for a read-only progress indicator. Pass size="md" for slightly chunkier dots.
Understanding the component
- Four states per dot.
pending,current,correct,wrong— surfaced asdata-stateon each dot so style overrides hook in cleanly. - Controlled + uncontrolled. Pass
currentfor controlled; otherwise the row tracks its own active index fromdefaultCurrent(Radix-style). The two modes never mix. resultswins overcurrent. A non-undefinedentry inresultspaintscorrect/wrongregardless ofcurrent. Missing entries fall back to pending / current.- Interactive mode. Passing
onSelectswitches the row torole="tablist"withrole="tab"buttons, arrow / Home / End keyboard nav, and a rovingtabIndex. Without it, the row isrole="group"with plain<span>dots. - Data hooks. The root carries
data-cb-progress-dots,data-size,data-current,data-total. Each dot carriesdata-stateanddata-indexso test selectors can target a specific step.
Variants
Passive indicator
<ProgressDots total={6} defaultCurrent={3} />With per-step results
<ProgressDots
total={5}
defaultCurrent={3}
results={[true, true, false, undefined, undefined]}
/>Interactive (tappable)
<ProgressDots
total={6}
current={step}
onSelect={setStep}
size="md"
aria-label="Pick a step"
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
total | number | required | Number of dots to render. |
current | number | — | Controlled active step index. Pair with onSelect. |
defaultCurrent | number | 0 | Uncontrolled initial active step index. |
results | (boolean or undefined)[] | — | Per-step correct / wrong markers. Missing entries are pending / current. |
onSelect | (index: number) => void | — | Tap / keyboard handler. Presence flips the row to interactive mode. |
size | 'sm' or 'md' | 'sm' | Dot size. sm = 8px, md = 10px. |
aria-label | string | 'Step progress' | Accessible label for the row. |
className | string | — | Merged onto the root via cn(). |
Accessibility
- Passive rows render as
role="group"with anaria-label; each dot is a span with a per-steparia-labellikeStep 2 of 5 — current. - Interactive rows render as
role="tablist"with each dot asrole="tab". Only the active tab hastabIndex={0}— focus rolls through with arrow keys, Home, and End. - Every interactive dot has a focus-visible ring (
ring-cb-ring) for keyboard users. - Color contrast: accent (current) / success (correct) / error (wrong) all pass WCAG AA against
cb-bg-elevated. - Motion is limited to a 150ms CSS transition on
background-color/transform/opacity— well under the 300ms ceiling and inherently respectful ofprefers-reduced-motionbecause no JS animation is involved.
Credits
- Extracted from:
algoflashcards(src/lessons/primitives/chrome/ProgressDots.tsx). The original was a thin wrapper over aStepChromecompound with track-accent /hexcoloring and bespoke motion. craft-bits keeps the same shape (total,current,results) but trims the source-project context (StepChrome, accent hex, lesson-specific motion) and adds a tappable mode for stepper navigation.