Skip to content
Back to skills

interactive-product-demo

ASecurity

Build a product demo out of live DOM and CSS instead of a video or GIF: a replica of the real UI, a scripted cursor, a camera that pans and spotlights each control, and a beat engine that runs only while the demo is on screen. Use when building a hero animation, product showcase, landing-page demo, feature walkthrough, animated screenshot, onboarding tour replay, or when replacing a screen recording / Loom / GIF on a marketing page. Triggers: "animate the product", "show the product in action...

  • 3 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 2, 2026
developmentgonodedebugginggit

Works with

  • cursor
  • cli

Security analysis

A100/100

Pro scans all 19 files and shows the line behind each finding

Scanned October 2, 2026

npx -y skills add noumansarwarr/interactive-product-demo --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of interactive-product-demo?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for interactive-product-demo
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/noumansarwarr-interactive-product-demo/badge)](https://www.skillsdirectory.com/skills/noumansarwarr-interactive-product-demo)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: interactive-product-demo
description: >-
  Build a product demo out of live DOM and CSS instead of a video or GIF: a replica of the
  real UI, a scripted cursor, a camera that pans and spotlights each control, and a beat
  engine that runs only while the demo is on screen. Use when building a hero animation,
  product showcase, landing-page demo, feature walkthrough, animated screenshot, onboarding
  tour replay, or when replacing a screen recording / Loom / GIF on a marketing page.
  Triggers: "animate the product", "show the product in action", "showcase scene",
  "interactive product demo", "hero animation", "scripted cursor demo",
  "demo without recording a video". Once the demo ships it can also export it as an
  MP4, GIF or WebM, but only if the user says yes when asked.
---

# Interactive Product Demo

Build a product demo out of real DOM, real components, and CSS transitions. A scripted
cursor walks through a replica of the actual app while a camera pans and spotlights the
control being used.

## What this is not

Not a screen recording, not a GIF, not a Lottie export, not a canvas/WebGL scene, not a
carousel of screenshots. Nothing is pre-rendered. The demo is the product's own components
fed fixture data, driven by a small state machine.

You get: no media payload, text that stays sharp and selectable-by-the-DOM, layout that
responds to the container, copy/colors/states editable without a re-export, and a demo that
does not go stale the day the UI changes.

## The seven invariants

Violate these and the demo reads as broken, not as a product.

1. **The click lands before the state it causes.** Derive demo state from *committed*
   beats (`done = phase === 'done' ? index : index - 1`), never from the current index. If
   the table updates while the cursor is still travelling, the demo looks like a slideshow.
2. **Keep the meaningful object mounted.** Change its state, geometry, and surroundings.
   Never cross-fade two copies of the same thing.
3. **Nothing animates off screen.** A demo that is mounted but not being watched must have
   zero pending timers. Visibility gating is a correctness requirement, not an optimization.
4. **Measure real targets; never hard-code coordinates.** The cursor follows a
   `[data-demo-target]` node measured in unscaled frame space.
5. **The cursor is a real object, or the story is a fake.** Two halves, and both are
   load-bearing. It touches nothing, so the control has to replay its own hover and
   press states or the click lands on a dead pixel. And it is continuous in space and
   time: it enters once, stays for the whole walkthrough including every hold, moves
   only by travelling, and leaves once. A pointer that blinks out between actions and
   reappears somewhere else has told the viewer the demo is a slideshow, and no
   amount of fidelity elsewhere takes that back.
6. **The demo must still read when the text does not.** On a phone a 1400px frame
   lands near a third of its authored scale and body text is 4–5px. The camera's
   push-in is capped on purpose: magnifying further stops reading as a screen and
   starts reading as a crop. So the story is carried by silhouette, colour and
   motion, and anything that depends on reading a *word* belongs in the page copy
   around the demo, not inside the frame.
7. **The frame fills its stage, whole.** Two failures, one rule. Never slice the UI to
   fill the space: no half navbar, no column dropped to make things fit. And never
   letterbox to avoid slicing: dead bars beside the demo look like a broken embed and
   throw away the width you were given. Both mean the frame's aspect does not match
   the stage's, and the fix is to change one of them. You can. Both are yours.

## Workflow

Work top to bottom. Each step has a gate; do not proceed past a failing gate. Two of
the gates — step 0 and the step 4 checkpoint — are the user's confirmation, not your
own assessment.

### 0. Take the brief

Do not infer the demo from the surrounding page. Five things decide what gets built
and none of them are in the codebase:

| Unknown | What it decides |
| --- | --- |
| Screen and task | everything downstream; the most expensive thing to get wrong |
| Camera style | zoom-and-spotlight or flat — and with it, whether a browser frame earns its place |
| Placement and stage geometry | frame dimensions, aspect, whether a fixed nav must be cleared |
| Mobile posture | how hard the story has to work once the text is unreadable |
| Theme | which palette the replica restates on `.demo` |

**Camera style is a fork, not a detail.** Never assume it. Offer both:

- **Zoom and spotlight** — the camera pushes in on each control and dims the rest.
  Directs the eye hard, and it is the only thing that works in a slot too small to
  show the whole screen legibly. Costs context: the viewer never sees the product
  whole.
- **Flat, whole screen** — the frame sits still at full size and the cursor does all
  the work. Shows the product as a product, and every change stays in shot. Needs a
  stage wide enough to stay readable, and a beacon carrying the eye-direction the
  camera was doing.

A **browser frame** — a top bar with tab and the product's real URL — is an option the
user picks, never a call you make for them. Put it in the same question as the camera
style, with a recommendation attached, and let them answer. It costs frame height and
buys "this is a real page someone is using"; under a zoom camera it is cropped out of
shot for most of the loop, so the recommendation is usually "no" when the camera moves
and "yes" when it does not. Recommending is the job. Deciding silently, however
obvious the answer looks, is not — it is a visual choice about their product, and the
reasoning that makes it obvious to you is not visible to them.

Ask only for what the request has not already settled, and at most four questions in
**one** batched round — take them in the order of the table and give the rest a stated
default. A request that names the screen and the slot leaves two questions; a fully
specified one skips this step. Whatever is still open after the round gets a default,
not a second round.

Everything else is repo work, not a question: which components can be imported, the
real response types, the guard rules that decide what is actionable. Escalate to the
user only when the repo genuinely cannot answer — the app and the marketing site are
separate packages, the components need auth context.

Then write the brief back and wait for a yes. Draft it against the rule in step 1 —
one person, one task, one visible outcome — rather than as a transcript of the ask:

```
Task      one sentence: who, what they do, what changes  (becomes the aria-label)
Screen    route + the components that render it
Actions   3 or fewer, in order
Before    what the viewer sees at beat 0
After     what is different at the last beat
Camera    zoom-and-spotlight | flat whole-screen
Chrome    app only | inside a browser frame showing <the real url>
Frame     W×H, and why that size
Stage     where it sits on the page, and the aspect it gets
Mobile    does it ship to phones, and what carries the story at 4px text
Theme     dark/light + where the palette comes from
```

**Gate:** the user has confirmed the brief. Nothing is built before that.

### 1. Pick one task, not a feature tour

A demo shows **one person completing one task with one visible outcome**, in 8–15 seconds.
"Approve the three leave requests waiting on HR" is a demo. "Leave management" is not.
Write the one-sentence outcome down first; it becomes the `aria-label`.

**Gate:** you can name the before-state, the actions, and the after-state in one sentence.

### 2. Collect product truth

Open the real screens in the codebase and list: the route, the components that render it,
the fixture shape those components need, the exact labels/statuses/colors, and the guard
rules that decide what is actionable. Read code, do not guess. Do not invent generic SaaS
UI — the reason a demo feels real is that it *is* the product.

**Gate:** you have a list of real component imports and the props they need.

### 3. Build the still frame first

Chrome replica (sidebar, header, breadcrumb) + **the real page components** + fixture data.
No motion yet. Author the frame at fixed pixel dimensions (e.g. `1400 × 900`) sized so the
content that must stay legible fits without scrolling.

Reuse over rebuild: import the app's own table, badge, avatar, and toolbar components and
feed them fixtures. Hand-rolled mock UI drifts from the product within a sprint. See
`references/replica.md` for the reuse traps (CSS resets, portals, tokens) — they are the
main reason this step fails.

**Nothing is left out to make it fit.** The frame holds the screen as the product
renders it — every nav item, the whole header, the full toolbar. If it does not fit,
the frame is the wrong size or the story is the wrong story. Dropping UI is never the
answer (invariant 7).

**Match the frame's aspect to the stage's. Do this before you pick any dimension.**
This is the single decision that makes everything downstream work, and getting it
wrong produces the two visible failures in invariant 7:

1. **Measure the real slot**, at each width it ships at. Open the element the demo
   replaces and read its box. Do not guess from the design.
2. **Author the frame to that aspect.** Pick the width the content needs, then let the
   height follow the ratio. If the extra height leaves room, spend it on the product:
   more rows, real spacing. Do not letterbox it away.
3. **If the slot's aspect is wrong, change the slot.** It is your stylesheet. Set
   `aspect-ratio` on the panel hosting the demo so it matches the frame. A slot whose
   aspect changes across breakpoints, or that is square on phones while the screen is
   16:9, is a slot to pin, not a shape to letterbox into.

Only once the aspects agree do `contain` and `cover` mean the same thing, and only
then does **`push: 0`** mean what you want: the whole screen, filling the stage,
nothing clipped. Give it to every wide beat — the establish, any "pull back and hold",
the payoff. With mismatched aspects that same `push: 0` gives you bars, which is the
letterboxing half of invariant 7.

Clipping is then only ever something a beat asked for by pushing in, which is the deal
a viewer already accepts on a real site: the chrome stays whole until *they* zoom.

**Narrow stages are the camera's job, not a second build.** `useDemoCamera` ramps a
push-in continuously as the stage shrinks — no breakpoint, no second frame, nothing to
configure. What this step owes it is a frame whose *story* survives that ramp, because
the ramp is capped and the text does not survive it (invariant 6). Check the frame the
way a phone will show it: squint at it. If the before/after difference disappears when
you cannot read a word, the fix is here — a bigger status pill, a colour change, a row
that visibly empties — not in the camera.

**Gate:** the static frame is indistinguishable from a screenshot of the real screen —
nothing omitted, nothing simplified — and its before/after difference is still visible
with the text unreadable.

### 4. Write the beat script as data

```ts
const BEATS: readonly DemoBeat[] = [
  { hold: 1000, id: 'queue' },                                            // establish
  { hold: 1100, id: 'ping' },                                             // something arrives
  { hold: 420, id: 'open', target: NOTICE, travel: 900, zoom: 1.12 },     // act
  { hold: 1700, id: 'arrived' },                                          // read the result
  ...
]
const AT = { arrive: 3, ping: 1 } as const   // which beat index commits which state
```

A beat with a `target` is an action; a beat without one is a hold that lets the viewer read
what just changed. Alternate them. Timing budget in `references/authoring.md`.

**Gate:** every state change in the story maps to exactly one beat index in `AT`.

**Checkpoint:** show the user the `BEATS` array and the `AT` map before wiring anything.
The script is the whole story in a form that reads in ten seconds, and this is the last
cheap moment to redirect it.

### 5. Wire the primitives

Copy from `assets/` (they are dependency-free and framework-thin):

| File | Owns |
| --- | --- |
| `use-demo-beats.ts` | `travel → press → done` per beat, pause-resume that banks unspent time |
| `use-demo-active.ts` | the one true "is anyone watching" signal (intersection + panel liveness + tab visibility) |
| `use-demo-camera.ts` | fit/cover scale, the narrow-stage ramp, damped pan to target, wide-target aiming, cursor point, focus box |
| `use-typed-text.ts` | character/line-wise reveal for chat or console output |
| `demo.css` | spotlight vignette (the blur layer is opt-in), cursor, press ripple, reduced-motion and narrow-viewport fallbacks |
| `demo-template.tsx` | a complete skeleton wiring all of the above |

**Gate:** `reset()` runs when the demo goes inactive, so every viewer starts at beat 0.

### 6. Derive everything from committed beats

```tsx
const done = phase === 'done' ? index : index - 1
const arrived   = reduced || done >= AT.arrive
const selected  = !reduced && done >= AT.select && !committing
```

One derivation block, then pass plain booleans down. Do not `useState` + `useEffect` your
way to the same values — that reintroduces ordering bugs on fast scroll.

**Gate:** scrubbing fast through the demo never leaves a stale cursor, menu, or toast.

### 7. Degrade deliberately

- `prefers-reduced-motion`: render the **end state**, no camera, no cursor, no spotlight.
  Handle it in JS (stop scheduling) *and* CSS (kill transitions) — either alone leaks.
- Narrow viewports: nothing to implement. The camera's ramp handles the scale; use a
  beat's `push` (0 for "show the whole screen", above 1 for "this one has to be read")
  where the default shot is wrong. `references/authoring.md` has the numbers.
- `role="img"` + one `aria-label` sentence on the demo root; `aria-hidden` on cursor,
  spotlight, beacons, and decorative SVG.
- `pointer-events: none` on the frame; `user-select: none` on the demo.

**Gate:** the reduced-motion render alone still communicates the outcome.

## Reference map

Load only what the current step needs.

| File | Read when |
| --- | --- |
| `references/architecture.md` | wiring the layers, camera math, state derivation |
| `references/replica.md` | reusing real app components inside a marketing page |
| `references/authoring.md` | writing the beat script, timing, story rules, cursor legibility |
| `references/pitfalls.md` | anything looks wrong — read this before debugging |
| `references/checklist.md` | before shipping |
| `references/video.md` | only after the user said yes to a video (see Video export) |

## Ship gate

Run the project's own checks (formatter, linter, type-check), read the diff, then walk
`references/checklist.md`.

Then verify it in a browser. Most of what goes wrong with a demo cannot be settled by
reading code: whether the frame fills the stage or sits in bars, whether the chrome is
whole on the wide beats, whether the cursor reads against the replica's surface,
whether the click lands before the state it causes. Those are the failures that reach
the user, and every one of them is obvious on sight and invisible in a diff.

If the user has named a tool, use it. If not, ask which one — do not pick one and
start driving it, and do not skip the step because none was named. Drive the real page
at more than one width, step the beats, and report what you saw rather than what the
code implies.

## Video export (opt-in)

Only after the ship gate has passed, and only with the user's yes. The live demo is
the deliverable; a video is a copy of it for places that cannot run code.

**Ask first, once.** One question, with "no" as the default:

> Do you also want a video file of this demo, for places that can't run it live
> (social posts, a GitHub README, email, a launch post)? No / MP4 / MP4 + GIF

- **No, or no answer:** stop. Do not record, and do not ask again.
- **The original request already asked for a video:** that is the yes; skip the
  question and use the formats it named (MP4 if none).
- **Yes:** read `references/video.md` and follow it.

**Recording is read-only.** It never changes the demo, its `BEATS`, its hooks or its
stylesheet. If the recording looks wrong, the demo is wrong: fix it, re-run the ship
gate, then record again.

**Gate:** you looked at the video's frames and poster, not just the exit code, and
they pass the checklist in `references/video.md`.

Files in this skill

  • .claude-plugin/marketplace.json488 B
  • .claude-plugin/plugin.json723 B
  • CHANGELOG.md12.2 KB
  • CITATION.cff864 B
  • SKILL.md16.3 KB
  • assets/demo-template.tsx7.6 KB
  • assets/demo.css11.3 KB
  • assets/record-demo.mjs12.2 KB
  • assets/use-demo-active.ts2.7 KB
  • assets/use-demo-beats.ts3.9 KB
  • assets/use-demo-camera.ts10.8 KB
  • assets/use-typed-text.ts1.5 KB
  • llms.txt3.3 KB
  • references/architecture.md7.8 KB
  • references/authoring.md8.9 KB
  • references/checklist.md4.8 KB
  • references/pitfalls.md9 KB
  • references/replica.md7.9 KB
  • references/video.md4.8 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…