Draw on
FreeWraps an inline SVG so its strokes draw themselves in, one after another, when it scrolls into view. Nodes can fade in after their line arrives.
pnpm dlx shadcn@latest add https://caliperui.site/r/draw-on.jsonWhen to use #
Use it for
- Diagrams, routes and underlines that tell a story in order.
- A check mark or chart line that should feel drawn, not placed.
Skip it when
- Icons and anything in a list; drawing is for a moment, not for rows.
- Filled artwork; only strokes draw.
Decisions #
Why it is built this way, so you know what to keep when you change your copy.
Behaviour
- Order is document order
- Each stroke starts 140ms after the one before it, so the order you write the shapes in is the story the drawing tells. Nothing is communicated by order alone.
- Without script it is already drawn
- Strokes only start hidden once the part is ready, so a page without JavaScript, and reduced motion, show the finished drawing.
Usage
- Replay with a key
- Change the key to play it again. The SVG still needs its own accessible name.
Installation #
pnpm dlx shadcn@latest add https://caliperui.site/r/draw-on.json
Adds its 2 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.
Usage #
This is the code behind the preview above.
import { useState } from "react" import { Button } from "@/components/ui/button" import { DrawOn } from "@/components/ui/draw-on" export function DrawOnDemo() { const [run, setRun] = useState(0); return ( <div className="grid w-full max-w-lg gap-3"> <Button size="sm" variant="secondary" className="justify-self-end" onClick={() => setRun((count) => count + 1)}>Replay</Button> <DrawOn key={run}> <svg viewBox="0 0 560 220" role="img" aria-label="A route from commit to build to release, with a rising line and a completed check" className="h-auto w-full text-foreground"> <g fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <path d="M26 36 H210" /> <path d="M70 150 C140 150 150 96 230 96 S340 150 420 118 S480 96 520 96" /> <circle cx="520" cy="96" r="20" strokeWidth="1.5" /> <path d="M510 96 l7 8 l14 -16" /> <path d="M26 196 L120 190 L200 198 L300 176 L400 182 L540 160" strokeWidth="1.5" className="text-border-strong" /> </g> <g fill="var(--elevated)" stroke="currentColor" strokeWidth="2"> <circle data-draw="fade" cx="70" cy="150" r="9" /> <circle data-draw="fade" cx="230" cy="96" r="9" /> <circle data-draw="fade" cx="420" cy="118" r="9" /> </g> <g className="fill-muted-foreground font-mono text-[11px]"> <text x="26" y="24">Your pipeline, drawn</text> <text x="48" y="184">commit</text> <text x="208" y="130">build</text> <text x="398" y="152">release</text> </g> </svg> </DrawOn> </div> ); }
API reference #
DrawOn
component
Also accepts the native props of its root element and a ref.
Accessibility #
It is built on native elements, so roles and focus come from the browser.
No custom key handling; Tab and the native activation keys do the work.
Motion #
It declares no animation in its component source. Any transition comes from its stylesheet, which uses the duration tokens that collapse to instant under prefers-reduced-motion.