Product demo
Play your real UI like a screen recording: a scripted cursor that presses real buttons, a ripple at each press, a zoom that pushes in on the result and captions underneath, chained on one timeline.
4 free components · MIT · every piece also works alone
The scene
The scene installs with every piece it uses.
npx shadcn addThe pieces
A pointer for product demos over live UI, not a video.
A press ring for clicks you want seen in demos, recordings and tutorials.
Pushes in on a region of live UI the way a screen recording does: the view scales and pans on the smooth spring until the target sits centred, the rest dims behind a cutout that follows its corners, and it springs back when you let go.
Short captions that step in under a scene with Text Swap's motion: the old line lifts and blurs out, the new one rises in, and the width springs to fit.
One timeline
useDemoTimeline from eased-demo-core is one clock of named steps: each piece reads the current step, and the cursor and the zoom find their targets by data-demo name, so the scene survives layout changes. It pauses off-screen and in a hidden tab, and under reduced motion it rests on endStep.
const STEPS = [ { id: "rest", duration: 1000 }, { id: "aim", duration: 800 }, { id: "press", duration: 160 }, { id: "live", duration: 2400 },] as const;export function Scene() { const stage = useRef<HTMLDivElement>(null); const { step } = useDemoTimeline(STEPS, { endStep: "live", observe: stage }); return ( <div ref={stage} inert className="relative"> <ZoomSpotlight active={step === "live"} target="result"> <Button data-demo="deploy">{step === "live" ? "Deployed" : "Deploy"}</Button> <Card data-demo="result"> <CardContent>…</CardContent> </Card> <DemoCursor to={step === "rest" ? "result" : "deploy"} pressed={step === "press"} /> <ClickRipple trigger="demo" /> </ZoomSpotlight> <CaptionTicker captions={["Press Deploy", "Live in 41 seconds"]} index={step === "live" ? 1 : 0} /> </div> );}Need a guided product tour? Spotlight Tour in Pro.