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

Usage

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 flex column. Spreads unknown props onto a <div> that carries data-cb-edu="race-panel" and data-step so consumer apps can hook custom styles to the current step.
  • Caption — small muted prose above the racer grid. Reused as the grid's aria-label when 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 itself role="group" with a derived aria-label.
  • Value — wrapped in AnimatePresence mode="popLayout" and keyed by the step index so every change re-mounts and fades in.
  • Progress barrole="progressbar". Width animates on SPRINGS.smooth from the previous fraction to the current one; reduced-motion users snap.
  • Badge — opt-in per step ("DONE", "WINNER", …). Sits inside role="status" aria-live="polite" so screen readers announce it on first appearance.

Props

PropTypeDefaultDescription
racersreadonly RacePanelRacer[]requiredThe racers to compare side by side.
currentStepnumberrequiredThe step index every racer is currently displaying.
columns2 | 3 | 4derivedNumber of columns at the sm breakpoint. Defaults to the racer count, capped at 4.
captionReactNodeOptional caption rendered above the racer grid.
transitionTransitionSPRINGS.smoothOverride bar / value transitions. Reduced-motion users snap regardless.
classNamestringMerged onto the root via cn().

RacePanelRacer

FieldTypeDescription
idstringStable React key + a11y identifier.
labelReactNodeLane heading rendered above the value.
stepsreadonly RacePanelStep[]Ordered list of states.
toneRacePanelToneSemantic tone for the lane. Defaults to accent.
subLabelReactNodeOptional sub-label rendered under the heading.

RacePanelStep

FieldTypeDescription
valueReactNodePrimary value rendered in the racer's lane at this step.
captionReactNodeOptional caption rendered below the value.
progressnumberOptional override for the default progress fraction. Clamped to 0..1.
badgeReactNodeOptional 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 an aria-label derived 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" with aria-valuenow / aria-valuemin / aria-valuemax reflecting 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 compound Race.Layout / Race.Panel / Race.Callout trio plus a useCountUp hook, with inline hex track colors threaded through every lane. The library extract collapses the trio into a single controlled component: callers pass a racers array plus a currentStep index, and tone tokens replace the inline track hex.