Swap Animator
A linear row of values plus an ordered list of [fromIdx, toIdx] swap pairs. Advance currentStep from 0 (nothing swapped) to swaps.length (every swap applied) and the component replays the swaps onto the array, morphing each cell to its new slot via a shared layoutId. Drives selection sort, bubble sort, the quicksort partition step, and any in-place index-pair-swap algorithm.
Fully controlled. (values, swaps, currentStep) is the source of truth — the same triple always produces the same layout. No internal history, no race conditions, no off-by-one drift.
Starting array: 5, 2, 8, 1, 9, 3.
Initial array
5
2
8
1
9
3
0 / 5
Installation
npx shadcn@latest add https://craftbits.dev/r/swap-animator.jsonUsage
import { SwapAnimator } from "@craft-bits/core";
<SwapAnimator
values={[5, 2, 8, 1, 9, 3]}
swaps={[[0, 3], [1, 5], [2, 5], [3, 4], [4, 5]]}
currentStep={2}
/>Step through a swap trace from a parent reducer or timer:
const [step, setStep] = useState(0);
const swaps = useMemo(() => selectionSortTrace(values), [values]);
<SwapAnimator
values={values}
swaps={swaps}
currentStep={step}
/>Highlight the active swap in a warning tone for a "watch this" beat:
<SwapAnimator values={values} swaps={swaps} currentStep={step} tone="warning" />Understanding the component
- Pure step replay. The plan is derived from
(values, swaps, currentStep)alone. Going forward and backward incurrentStepis symmetric — there is no internal history to keep in sync. - Shared-layout morph. Each entry carries a stable
layoutIdthat follows the value across every swap. WhencurrentStepadvances, Framer Motion morphs the cell from its old slot to its new slot in a single spring rather than unmount-remounting in place. - Active-pair highlight. The two indices participating in the swap at
currentStepare tinted with the selectedtoneand gain a thicker inset ring. Every other cell stays neutral. - Five tones.
defaultis neutral;accentis the library default;successis "winning swap";warningis "watch this";erroris "wrong swap". The active cells also gain a contrasting fill, so colourblind users see the highlight. - Hit target. Every cell is at least 44 by 44 px regardless of the
cellSizeprop, so narrow visual cells still satisfy WCAG 2.5.8 AAA on touch screens. - Reduced motion. The shared-layout morph collapses to instant under
prefers-reduced-motion: reduce. The swap still applies; only the motion drops.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
values | number[] | required | Starting array. Swaps are applied to a copy. |
swaps | [number, number][] | required | Ordered list of index-pair swaps. |
currentStep | number | required | How many swaps to have applied so far. Clamped to swaps length. |
tone | "default" | "accent" | "success" | "warning" | "error" | "accent" | Highlight palette for the active swap pair. |
showIndices | boolean | true | Show a small index label below each cell. |
cellSize | number | 44 | Cell width / height in pixels. |
cellGap | number | 6 | Gap between cells in pixels. |
caption | ReactNode | — | Optional caption rendered above the row. |
transition | Transition | SPRINGS.smooth | Override the swap transition. Reduced-motion users snap regardless. |
className | string | — | Merged onto the root via cn(). |
Accessibility
- A visually-hidden
aria-live="polite"region narrates the current swap pair and the running array, so screen readers stay current ascurrentStepadvances. - Every cell carries
role="img"and an explicitaria-labelnaming the slot index, value, and whether it is participating in the current swap. - The root exposes
data-state(swapping/complete) anddata-tone, and every cell exposesdata-state(active/rest) so consumer apps can hook custom styles or assistive tooling. - Each cell is at least 44 by 44 px regardless of the
cellSizeprop so narrow visual cells still satisfy WCAG 2.5.8 AAA on touch screens. - Motion respects
prefers-reduced-motion: reduce— the shared-layout morph collapses to instant. The swap still applies; only the motion drops.
Credits
- Extracted from:
algoflashcards(src/lessons/primitives/viz/SwapAnimator.tsx). The source was an imperative overlay animator that measured DOM rects viaquerySelectorand floated absolutely-positioned overlay divs from one cell to another. The library extract collapses to a fully declarative(values, swaps, currentStep)triple and uses Framer Motion's sharedlayoutIdinstead of manual rect measurement — so the same component covers parent-driven scrubbing, autoplay timers, and reverse playback from one API.