playthru
Add a scripted, looping product demo to a website using the playthru library. A fake cursor glides over the real UI, clicks, types and zooms the camera in, with no video file. Use when the user wants a hero demo, product walkthrough, animated "how it works" section, or a self-playing demo of their app on a landing page, or mentions playthru.
Install
npx skills add https://github.com/diip3sh/playthrou --skill playthruplaythru
playthru plays a scripted demo on the site's real UI. A fake cursor glides, clicks real elements, types into real inputs, and a camera zooms in on what matters, then it loops. It has no dependencies, is about 5 KB, and there's no stylesheet to import. Every timing is fixed by the library: there is no speed, easing or duration option, so don't look for one.
The user may not know npm or JavaScript tooling. Do the setup for them, choosing from the paths below, and explain in plain words what you changed.
1. Pick the setup path
Look at the project first:
A. npm. Run the install with the project's package manager (look for pnpm-lock.yaml, yarn.lock or bun.lock, and use npm if there's none):
B. CDN, no install. This works in any .html file. The script tag must have type="module":
Put this script after the stage markup, or at the end of <body>.
2. Make the stage
The stage is the element the demo plays inside. Usually it's a box on the landing page holding the app's UI (or a static copy of it).
- Give it a fixed height (e.g.
height: 520px). The camera scales the content inside it, and playthru setsoverflow: hiddenon it. - It should have one child, which becomes the "scene" the camera moves. If there are several, playthru wraps them for you.
- Everything the script touches must be inside the stage. Selectors are scoped to it.
- The UI must actually work: playthru fires real events, so the app's own click and input handlers do the work. If the "app" is a static mockup, add small handlers so clicks have a visible effect (open a modal, add a row).
- Give the targets stable selectors. Add
ids ordata-*attributes rather than relying on fragile class chains.
3. Write the script
Chain steps. They run in order, and the chain is the whole demo:
By default it starts the first time the stage is 30% in view and loops forever.
A target is a CSS selector (scoped to the stage) or a DOM element.
Options are the second argument: playthru("#hero", { loop, autoplay, pauseOnHover }). All three default to true. pauseOnHover freezes the demo and hides the fake cursor while a real mouse is over the stage, so visitors can try the UI.
Controls on the returned object: .play(), .pause(), .destroy(), .playing. Set autoplay: false and call .play() to start it from a button.
Good demo shape: 4 to 8 steps that tell one story (open something, fill it in, see the result). Zoom on the part being worked on, then zoomOut() before showing the result. Always write a reset that undoes everything the script changed, or the second loop will start from the wrong state.
4. React
Or attach to an element the app already renders: usePlaythru(ref, script, options). It creates the demo once per mount and destroys it on unmount.
- In Next.js App Router, the file using
PlaythruorusePlaythruneeds"use client"at the top. - To reset React state between loops, call the component's own setters inside
reset, e.g.d.reset(() => setTodos(initial)). - In other frameworks (Vue, Svelte, Astro islands), call
playthru(el)after mount and.destroy()on unmount.
5. Hover and focus styles
The fake cursor can't trigger CSS :hover, and playthru never moves real focus, so it can't trigger :focus either. Instead it marks elements with attributes. Mirror the existing styles onto them:
In Tailwind, use them as variants: hover:bg-gray-200 data-playthru-hover:bg-gray-200.
6. Gotchas
- A selector that matches nothing stops the demo with
playthru: nothing matches "…" inside the stagein the console. Check that each target exists inside the stage at the moment its step runs. - Links and submit buttons don't navigate. Their default action is prevented. If a click should do something, it needs a JS click handler.
- Menus outside the stage. The demo's events reach the whole page. While a menu or popover outside the stage is open, the demo pauses. It detects this through
aria-expanded="true"(witharia-haspopupor a combobox) or:popover-open. A hand-rolled popup with neither should ignore events whereevent.isTrusted === false. - Can't target elements inside iframes,
<canvas>or WebGL. - The drawn text caret only appears in single-line inputs. Textareas and contenteditable get typed into without one.
- With
prefers-reduced-motionthe cursor jumps, the camera stays still, and loops cut without fades. This is intended. - On small screens, if the zoom target can't fit at 1.3×, the camera stays out. This is intended.
7. Check it
Open the page: start the project's dev server, or for plain HTML serve the folder with npx serve or any static server, because ES modules don't load from file://. Scroll the stage into view and watch one full loop, then a second to confirm reset restores the start state. Check the browser console for playthru: errors.