Skip to content
Apps— browse docs
On this page

Step 3: The morphing tool palette

Four pens, a colour, a size — a bar that changes shape instead of swapping panels.

This toolbar is worth slowing down on as UI design on its own: pick a colour and the pen row becomes a palette; open the size control and it becomes a slider — never a second panel sliding in from somewhere else. One bar, three shapes.

Local state, not document state

Which pen you're holding is yours alone — it isn't something two people converge on, so it never touches strokes. It's plain useState, same as the camera on the pin board:

const [tool, setTool] = useState<PenId>("pen")
const [color, setColor] = useState("#151515")
const [width, setWidth] = useState(4)

The morph

One bar, one mode, three shapes. Selecting a colour or opening size doesn't mount a new component next to the old one — it re-renders the same bar with different children while its width animates, so the pill visibly grows or shrinks around whatever's inside it:

const [mode, setMode] = useState<"tools" | "color" | "size">("tools")
const barWidth = mode === "tools" ? 268 : mode === "color" ? 232 : 208

<div style={{ width: barWidth, transition: MORPH, overflow: "hidden", borderRadius: 999, /* … */ }}>
  {mode === "tools" && <>{/* four pens + a colour dot + a size dot */}</>}
  {mode === "color" && <>{/* a back arrow + six swatches */}</>}
  {mode === "size" && <>{/* a back arrow + a slider */}</>}
</div>
Note:

Why this isn't a real spring

vvd's actual motion contract is springs, not durations — @vvd/theme's springs/easings are physical, interruptible, and meant to drive motion/react's transition prop. Neither package can run in this page's sandbox (same reason as @vvd/editor-sdk in Step 2), so MORPH below is a plain CSS transition — but not an arbitrary one. cubic-bezier(0.34, 1.46, 0.5, 1) is the literal value @vvd/theme exports as easings.springOut / springOutCss, for exactly this situation: "a CSS transition that reads spring-like where a real spring isn't available." In a real build, skip the constant and reach for springs.snappy directly.

The morphing palette
src/codec.ts + src/tool.tsx Lines 31 to 40 and 52 to 104 highlighted.

Edit and the example re-runs. Tab indents; press Escape to leave the editor.

Running · your edits, live
Starting the example…

You should see: a rounded bar in the corner with four dots (each sized to its pen's width), a colour dot, and a size dot. Click the colour dot — the bar narrows and a row of swatches slides into view where the pens were. Pick one; it narrows again, back to the pen row, now showing your choice. Click the size dot for the same trick with a slider. Draw with each pen and watch the eraser genuinely erase (it paints in the canvas's own background colour).

Recap

  • Which pen you're holding is local UI state — it never enters the document, same as a camera.
  • A morph is one element whose width/children change together, not two elements swapping.
  • The curve is @vvd/theme's real easings.springOut value, inlined because the package can't run in this sandbox.

Next steps