AnimatedDiagram

A controlled shell for stepped algorithm diagrams. The caller owns the visual body (typically an SVG or a row of cells) as children; the shell owns the caption ribbon and the advance/replay button. Pair currentStep with onStep to drive the diagram from your own state — the body re-renders against the new step prop while the caption cross-fades and the button announces "advance" or "replay" depending on position.

target 12
1
3
5
7
9
11

L points at 1, R points at 11. Sum is 12 — target hit.

Installation

npx shadcn@latest add https://craftbits.dev/r/animated-diagram.json

Usage

import { AnimatedDiagram, type AnimatedDiagramStep } from "@craft-bits/core";
 
const steps: AnimatedDiagramStep[] = [
  { caption: "L points at index 0, R at index 5." },
  { caption: "Sum is 12 — target hit." },
  { caption: "Slide R left to keep searching." },
];
 
const [step, setStep] = useState(0);
 
<AnimatedDiagram steps={steps} currentStep={step} onStep={setStep}>
  <YourDiagramSvg step={step} />
</AnimatedDiagram>

The component is purely controlled — currentStep always reflects the caller's state. Each tap fires onStep(idx + 1); the last step's tap wraps back to 0 so a single button toggles between "advance" and "replay" without any extra wiring.

Read-only scrubber — omit onStep to render the diagram in playback mode without a button:

<AnimatedDiagram steps={steps} currentStep={externallyDrivenStep}>
  <YourDiagramSvg step={externallyDrivenStep} />
</AnimatedDiagram>

Understanding the component

  1. Controlled, not internal. currentStep is the source of truth. The component does not own step state.
  2. Wrap on the last step. Tapping the advance button on the final step fires onStep(0) — the button morphs into "Replay" and the same callback is reused.
  3. AnimatePresence caption ribbon. Caption swaps cross-fade in popLayout mode with initial={false} so the first paint does not animate.
  4. aria-live="polite" announcement. Each step's caption string (or its ariaLabel override) is announced to screen readers on change.
  5. 44 by 44 px tap target. The advance/replay button enforces the WCAG 2.5.8 touch-target floor via padding, regardless of the visible chip size.
  6. Bounded indices. currentStep is clamped to [0, steps.length - 1] internally.
  7. Reduced-motion users get instant transitions — caption fade and tap spring collapse to { duration: 0 }.

Props

PropTypeDefaultDescription
childrenReactNoderequiredThe diagram body — typically an SVG or a row of cells.
stepsreadonly AnimatedDiagramStep[]requiredOrdered timeline of step descriptors.
currentStepnumberrequiredControlled active step index.
onStep(nextStep: number) => voidCalled when the user taps advance/replay. Omit to render the diagram read-only.
advanceLabelReactNode"Tap to advance"Label inside the button while more steps remain.
resetLabelReactNode"Replay"Label inside the button on the last step.
ariaLabelstring"Animated diagram"Accessible name for the diagram region.
classNamestringMerged onto the outer <div>.

AnimatedDiagramStep

FieldTypeDescription
captionReactNodeRendered in the ribbon while this step is active.
ariaLabelstringOptional override for the live-region announcement. Required when caption is non-string JSX.

Accessibility

  • The outer container is role="region" with a configurable aria-label.
  • The caption ribbon is aria-live="polite" with aria-atomic="true".
  • The advance/replay button's aria-label includes the current and total step counts.
  • The button's hit area is at least 44 by 44 pixels (WCAG 2.5.8 AAA).
  • Motion respects prefers-reduced-motion: reduce.
  • data-step and data-total-steps attributes mirror the controlled state for testing selectors.

Credits

  • Extracted from: algoflashcards (src/lessons/primitives/viz/AnimatedDiagram.tsx). The source was hard-wired to the two-pointer scan on a sorted array. The library extract narrows the shell to its reusable spine: a caller-owned body via children, a controlled currentStep + onStep pair, a steps array with caption + optional ariaLabel, and a single morphing advance/replay button.