Merge Viz
A primitive for the moment two things become one. Left source, right source, merged result in the middle; arrows point inward; as progress advances from 0 to 1 the two sources shrink, slide toward the centre, and fade out while the merged box fades in. Used to teach byte-pair encoding ("l" + "o" → "lo"), token concatenation, vocabulary merges, and LoRA / model merging — anywhere the lesson is "two ingredients combine into one product."
Merge visualisation: a and b into merged.Merge of a = l and b = o into merged = lo. Progress 0 percent.
0%
Customize
Pair
l + o
Playback
1200
Installation
npx shadcn@latest add https://craftbits.dev/r/merge-viz.jsonUsage
import { MergeViz } from "@craft-bits/core";
<MergeViz a="l" b="o" defaultPlaying />Drive the progress externally from a narration step:
const [t, setT] = useState(0);
<MergeViz
a="un"
b="happy"
merged="unhappy"
progress={t}
onProgressChange={setT}
/>Relabel the slots — useful when the merge isn't really a / b / merged:
<MergeViz
a="W_base"
b="ΔW_lora"
merged="W_merged"
labelA="base"
labelB="adapter"
labelMerged="deployed"
/>Understanding the component
- Three slots, one direction. The visual reads strictly left → centre ← right. Source
asits on the left, sourcebon the right, the merged result lives in the centre. Two arrows point inward so the eye knows where the energy is going before any animation happens. - Progress drives geometry. A single
progressvalue in[0, 1]interpolates everything — the source boxes'x, opacity, and scale, and the merged box's opacity and scale. The interpolation is smoothstepped so even a linear scrub feels eased. - Sources fade as the merged box arrives. Sources fade to
~15%opacity and shrink to~82%scale at full progress; the merged box stays invisible untilt ≈ 0.45and then springs in. The result is that the eye is never asked to track three solid boxes at once. - Controlled and uncontrolled — for both progress and playing. Pair
progress+onProgressChangeto drive from a parent (narration step, scrubber, lesson phase). Or passdefaultProgress+defaultPlayingand the component manages its own state. - Autoplay sweeps
0 → 1and resets. Whenplaying, an internalrequestAnimationFrameloop ramps progress toward1overplaySpeedms, holds at1for a beat, then snaps back to0for the next sweep. The loop is cancelled on unmount and whenplayingflips off — no orphaned timers. - Reduced motion is honest. With
prefers-reduced-motion: reduce, autoplay is force-stopped and every spring collapses to instant — the diagram still tells the story; it just doesn't move.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
a | string | required | Left source value. |
b | string | required | Right source value. |
merged | string | a + b | Result rendered in the centre box. |
progress | number | — | Controlled animation phase in [0, 1]. |
defaultProgress | number | 0 | Uncontrolled initial progress. |
onProgressChange | (progress: number) => void | — | Fires when progress changes (autoplay tick or scrub). |
playing | boolean | — | Controlled play state. |
defaultPlaying | boolean | false | Uncontrolled initial play state. |
onPlayingChange | (playing: boolean) => void | — | Fires when play / pause flips. |
playSpeed | number | 1200 | Milliseconds for one full 0 → 1 sweep. |
labelA | string | "a" | Label above the left source. |
labelB | string | "b" | Label above the right source. |
labelMerged | string | "merged" | Label above the merged box. |
transition | Transition | SPRINGS.smooth | Spring for the box position / size transitions. |
className | string | — | Merged onto the root via cn(). |
Accessibility
- The outer element is
role="figure"with anaria-labeland a visually hiddenaria-live="polite"summary — screen readers hear the two sources, the merged value, and the current progress percentage whenever it changes. - Colour is never the only signal — the merged box is also distinguished by a thicker accent stroke and bolder text, and arrows point inward toward it independent of colour.
- The play / pause button carries
aria-pressedand a descriptivearia-label; the scrubber is an<input type="range">with its ownaria-label="Merge progress". prefers-reduced-motion: reduceforce-stops autoplay and collapses every spring transition to an instant swap. The button is also disabled in that mode.
Credits
- Extracted from:
craftingattention(app/src/lessons/primitives/nn/MergeViz.tsx). The source was a LoRA-merging widget — a 2×2 matrix scene with an alpha slider forW_merged = W + α·A·B, per-cell colour-coded deltas, a phase machine, and a narration block. The library version drops the matrix-arithmetic conceit and ships the prior primitive every merge lesson actually needs: two source slots collapsing into one centre slot under a singleprogressparameter. Matrix-arithmetic merges remain a thin wrapper layer that any consumer can build on top by relabelling the slots and drivingprogressfrom anαknob.