Race Panel
A side-by-side race between two-or-more algorithms running the same task. The caller supplies a list of racers, each with a label and an ordered steps array, plus the currentStep index. The panel surfaces the racer's value at that step together with a progress bar, so two-or-more runs read as one comparison rather than two disconnected animations.
Comparisons used to find the target
Linear scan
0
O(n)
Scan begins.
Binary search
0
O(log n)
Search begins.
Customize
Timing
600
Racers
Installation
npx shadcn@latest add https://craftbits.dev/r/race-panel.jsonUsage
import { RacePanel } from "@craft-bits/core";
const RACERS = [
{
id: "linear",
label: "Linear scan",
subLabel: "O(n)",
tone: "warning",
steps: [
{ value: 0, caption: "Scan begins." },
{ value: 16, caption: "Compared 16 pairs." },
{ value: 49, caption: "All pairs visited.", badge: "DONE" },
],
},
{
id: "binary",
label: "Binary search",
subLabel: "O(log n)",
tone: "success",
steps: [
{ value: 0, caption: "Search begins." },
{ value: 2, caption: "Halved twice." },
{ value: 3, caption: "Target located.", badge: "WINNER" },
],
},
];
<RacePanel racers={RACERS} currentStep={step} />Out-of-range currentStep values clamp to each racer's own length, so a racer with a shorter timeline stalls on its final state while a longer racer keeps advancing — exactly the asymmetry that makes a complexity race readable.
Anatomy
- Root — a
flexcolumn. Spreads unknown props onto a<div>that carriesdata-cb-edu="race-panel"anddata-stepso consumer apps can hook custom styles to the current step. - Caption — small muted prose above the racer grid. Reused as the grid's
aria-labelwhen it is a string. - Racer lane — one per entry in
racers. Renders the label, value, optional sub-label, optional caption, progress bar, and optional badge inside a tone-tinted card. Each lane is itselfrole="group"with a derivedaria-label. - Value — wrapped in
AnimatePresence mode="popLayout"and keyed by the step index so every change re-mounts and fades in. - Progress bar —
role="progressbar". Width animates onSPRINGS.smoothfrom the previous fraction to the current one; reduced-motion users snap. - Badge — opt-in per step (
"DONE","WINNER", …). Sits insiderole="status" aria-live="polite"so screen readers announce it on first appearance.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
racers | readonly RacePanelRacer[] | required | The racers to compare side by side. |
currentStep | number | required | The step index every racer is currently displaying. |
columns | 2 | 3 | 4 | derived | Number of columns at the sm breakpoint. Defaults to the racer count, capped at 4. |
caption | ReactNode | — | Optional caption rendered above the racer grid. |
transition | Transition | SPRINGS.smooth | Override bar / value transitions. Reduced-motion users snap regardless. |
className | string | — | Merged onto the root via cn(). |
RacePanelRacer
| Field | Type | Description |
|---|---|---|
id | string | Stable React key + a11y identifier. |
label | ReactNode | Lane heading rendered above the value. |
steps | readonly RacePanelStep[] | Ordered list of states. |
tone | RacePanelTone | Semantic tone for the lane. Defaults to accent. |
subLabel | ReactNode | Optional sub-label rendered under the heading. |
RacePanelStep
| Field | Type | Description |
|---|---|---|
value | ReactNode | Primary value rendered in the racer's lane at this step. |
caption | ReactNode | Optional caption rendered below the value. |
progress | number | Optional override for the default progress fraction. Clamped to 0..1. |
badge | ReactNode | Optional badge rendered once this step is reached. |
Accessibility
- The root grid is
role="group"and labelled by the caption when it is a string. - Each racer lane is itself
role="group"with anaria-labelderived from the racer's label and current value, so screen readers announce the lane state as a single chunk. - Every progress bar is a
role="progressbar"witharia-valuenow/aria-valuemin/aria-valuemaxreflecting the racer's progress. - The badge slot uses
role="status"+aria-live="polite"so the "WINNER" / "DONE" announcement reaches assistive tech without stealing focus. - Tone is never the only signal — label, value, bar, and badge all carry the same status, so colour-blind users still parse the race.
- Motion respects
prefers-reduced-motion: reduce— bar fill, value pop, and badge entrance all collapse to instant.
Credits
- Extracted from:
algoflashcards(src/lessons/primitives/interaction/RacePanel.tsx). The source exposed a compoundRace.Layout/Race.Panel/Race.Callouttrio plus auseCountUphook, with inline hex track colors threaded through every lane. The library extract collapses the trio into a single controlled component: callers pass aracersarray plus acurrentStepindex, and tone tokens replace the inline track hex.