Magic Move Block
A before/after refactor diff — two or more code snapshots, shiki-tokenized, with matching tokens visually moving between positions instead of cross-fading. Inspired by Shiki Magic Move.
1. for-loop
function sum(nums: number[]) {
let total = 0;
for (let i = 0; i < nums.length; i++) {
total = total + nums[i];
}
return total;
}Customize
Step
1. for-loop
Playback
Installation
npx shadcn@latest add https://craftbits.dev/r/magic-move-block.jsonUsage
Pass an ordered array of snapshots. Each step is { code, lang?, title? }:
import { MagicMoveBlock } from "@craft-bits/core";
const BEFORE = `function sum(nums) {
let total = 0;
for (let i = 0; i < nums.length; i++) {
total = total + nums[i];
}
return total;
}`;
const AFTER = `function sum(nums) {
return nums.reduce((t, n) => t + n, 0);
}`;
<MagicMoveBlock
steps={[
{ code: BEFORE, lang: "tsx", title: "before" },
{ code: AFTER, lang: "tsx", title: "after" },
]}
/>Multi-step walkthrough
The component is not limited to two steps. Pass three or more snapshots and the prev / next / dot navigator lets the reader walk the refactor one transition at a time:
<MagicMoveBlock
steps={[
{ code: STEP_1, lang: "tsx", title: "1. for-loop" },
{ code: STEP_2, lang: "tsx", title: "2. for-of" },
{ code: STEP_3, lang: "tsx", title: "3. reduce" },
]}
defaultStep={0}
/>Controlled mode
Pair currentStep with onStepChange to drive the active step externally — useful when the step is tied to scroll position, a lesson phase, or a timeline scrubber:
const [step, setStep] = useState(0);
<MagicMoveBlock steps={steps} currentStep={step} onStepChange={setStep} />Auto-play
<MagicMoveBlock steps={steps} autoPlay autoPlayInterval={2400} />Auto-play is automatically skipped when prefers-reduced-motion: reduce is set.
Understanding the component
- Shiki tokenization. On every active-step change,
codeToTokensparses the active step's code into a 2D array of(content, color)pairs. The component flattens it to a per-line list it can render asmotion.spans. - Stable token identity. Each non-whitespace token's key is
${content}::${occurrence}whereoccurrenceis the 0-indexed instance of that content within the step (so twoconsts don't collide). The same key is used as a MotionlayoutId. - Shared
LayoutGroup. All steps render inside one<LayoutGroup>keyed by the component'suseId(). When the step swaps and the children remount, Motion sees the samelayoutIdelement appearing in a new position and tweens it from the old position — the "magic move." - Whitespace is plain. Indentation and inter-token spaces render as plain
<span>s, so the morph budget only spends on identifiers, keywords, and operators that actually carry meaning. - Reduced motion. When
prefers-reduced-motion: reduceis set, the morph transition collapses toduration: 0and auto-play stops — tokens snap to their new positions.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
steps | { code: string; lang?: string; title?: string }[] | required | Ordered list of code snapshots. |
currentStep | number | — | Controlled active step index. |
defaultStep | number | 0 | Uncontrolled initial step index. |
onStepChange | (step: number) => void | — | Fired when the active step changes. |
autoPlay | boolean | false | Cycle through steps automatically. |
autoPlayInterval | number | 2400 | Milliseconds between auto-play advances. |
hideNavigator | boolean | false | Hide the built-in prev / next / dot controls. |
theme | ThemeRegistration | neutral | Override the shiki theme. |
className | string | — | Merged onto the root <div>. |
Accessibility
- The code region is wrapped in
aria-live="polite"so screen readers announce the new code on every step change. - The dot navigator is a
role="tablist"with each dot arole="tab"carryingaria-selectedand aStep N of Maria-label. - Prev / Next buttons are real
<button>elements witharia-label="Previous step" | "Next step"anddisabledstate when only one step exists. - Focus is visible via a
focus-visible:outline keyed to--cb-accent. - Auto-play and the morph animation are fully disabled when
prefers-reduced-motion: reduceis set.
Credits
- Extracted from:
algoflashcards(src/platform/ui/MagicMoveBlock.tsx). The original wrappedshiki-magic-move/reactdirectly; craft-bits inlines the morph using Motion'slayoutIdso there's no third-party morph runtime. - Inspiration: Shiki Magic Move by @antfu.