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.
L points at 1, R points at 11. Sum is 12 — target hit.
Installation
npx shadcn@latest add https://craftbits.dev/r/animated-diagram.jsonUsage
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
- Controlled, not internal.
currentStepis the source of truth. The component does not own step state. - 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. AnimatePresencecaption ribbon. Caption swaps cross-fade inpopLayoutmode withinitial={false}so the first paint does not animate.aria-live="polite"announcement. Each step'scaptionstring (or itsariaLabeloverride) is announced to screen readers on change.- 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.
- Bounded indices.
currentStepis clamped to[0, steps.length - 1]internally. - Reduced-motion users get instant transitions — caption fade and tap spring collapse to
{ duration: 0 }.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | required | The diagram body — typically an SVG or a row of cells. |
steps | readonly AnimatedDiagramStep[] | required | Ordered timeline of step descriptors. |
currentStep | number | required | Controlled active step index. |
onStep | (nextStep: number) => void | — | Called when the user taps advance/replay. Omit to render the diagram read-only. |
advanceLabel | ReactNode | "Tap to advance" | Label inside the button while more steps remain. |
resetLabel | ReactNode | "Replay" | Label inside the button on the last step. |
ariaLabel | string | "Animated diagram" | Accessible name for the diagram region. |
className | string | — | Merged onto the outer <div>. |
AnimatedDiagramStep
| Field | Type | Description |
|---|---|---|
caption | ReactNode | Rendered in the ribbon while this step is active. |
ariaLabel | string | Optional override for the live-region announcement. Required when caption is non-string JSX. |
Accessibility
- The outer container is
role="region"with a configurablearia-label. - The caption ribbon is
aria-live="polite"witharia-atomic="true". - The advance/replay button's
aria-labelincludes 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-stepanddata-total-stepsattributes 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 viachildren, a controlledcurrentStep+onSteppair, a steps array withcaption+ optionalariaLabel, and a single morphing advance/replay button.