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

Usage

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

  1. Shiki tokenization. On every active-step change, codeToTokens parses 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 as motion.spans.
  2. Stable token identity. Each non-whitespace token's key is ${content}::${occurrence} where occurrence is the 0-indexed instance of that content within the step (so two consts don't collide). The same key is used as a Motion layoutId.
  3. Shared LayoutGroup. All steps render inside one <LayoutGroup> keyed by the component's useId(). When the step swaps and the children remount, Motion sees the same layoutId element appearing in a new position and tweens it from the old position — the "magic move."
  4. 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.
  5. Reduced motion. When prefers-reduced-motion: reduce is set, the morph transition collapses to duration: 0 and auto-play stops — tokens snap to their new positions.

Props

PropTypeDefaultDescription
steps{ code: string; lang?: string; title?: string }[]requiredOrdered list of code snapshots.
currentStepnumberControlled active step index.
defaultStepnumber0Uncontrolled initial step index.
onStepChange(step: number) => voidFired when the active step changes.
autoPlaybooleanfalseCycle through steps automatically.
autoPlayIntervalnumber2400Milliseconds between auto-play advances.
hideNavigatorbooleanfalseHide the built-in prev / next / dot controls.
themeThemeRegistrationneutralOverride the shiki theme.
classNamestringMerged 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 a role="tab" carrying aria-selected and a Step N of M aria-label.
  • Prev / Next buttons are real <button> elements with aria-label="Previous step" | "Next step" and disabled state 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: reduce is set.

Credits

  • Extracted from: algoflashcards (src/platform/ui/MagicMoveBlock.tsx). The original wrapped shiki-magic-move/react directly; craft-bits inlines the morph using Motion's layoutId so there's no third-party morph runtime.
  • Inspiration: Shiki Magic Move by @antfu.