pro-visu docs
Generators

interaction

Realtime recordings of scripted interactions — a synthetic cursor driving the page, or a clip cropped to one component.

For AI / LLMs: View Markdown

interaction records the page live while a scripted sequence plays out — a visible synthetic cursor moves, clicks, hovers, and types, exactly like a hand-recorded product demo but reproducible from config. It always records realtime (interactions and their animations are inherently time-based) and always emits a single asset.

{
  name: "menu-demo",
  url: "https://your-site.com",
  generator: "interaction",
  options: { actions: [{ do: "click", selector: "#menu-button" }] },
}

Give it actions (a scripted tour), focus (a clip cropped to one component), or both — one of the two is required. Site cleanup (hide the cookie banner, block trackers, seed a session cookie for logged-in flows) comes from settings.capture, same as every other URL capture — media keeps playing during the recording, since an interaction is live by design.

Config options

An interaction needs actions, focus, or both; everything else is optional. Reference is the interactive view; TypeScript is the same shape in code, every option at its default.

outputOutput

Video output: size, scale, frame rate, encoding, filename.

widthnumberdefault 1280

Output width in CSS px.

heightnumberdefault 800

Output height in CSS px.

deviceScaleFactornumberdefault 2

Render scale, max 4 — higher captures crisper, then downscales into the video.

fpsnumberdefault 30

Output frames per second (max 120).

crfnumberdefault 18

x264 quality, 0–51 (lower = better quality / larger file).

fileNamestring

Output filename. Defaults to "<slug(asset name)>.mp4".

pagePage

Page load + interaction timing.

startDelayMsnumberdefault 500

Dwell before the first step (ms).

endDwellMsnumberdefault 800

Dwell after the last step (ms).

waitUntil'load' | 'domcontentloaded' | 'networkidle' | 'commit'default 'networkidle'

Page-load milestone to wait for before recording.

waitForSelectorstring

Optional element to wait for before recording, e.g. a hero section. Omit to skip.

stickyHeaderHeightnumberdefault 0

Height (px) of a sticky/fixed header. scrollTo keeps targets clear of it: top-align drops them below it (coalesced with the target's own CSS scroll-margin-top — the larger wins, not summed — then offset stacks on top), center-align uses half of it, bottom-align is unaffected.

actionsAction[]default []

The scripted steps, performed live with a visible synthetic cursor. Each step's do value picks its shape (below). Steps run back-to-back with no built-in pause — insert a wait step to hold between them. Omit only when focus alone (scroll-into-view + a wait) is enough — an interaction needs actions and/or focus.

Each item is one of these shapes (its do picks which):

do: "move"Glide the cursor to a target without clicking. Give a selector, or x/y for empty space.
selectorstring

Element to glide onto. Omit to steer by x/y.

xnumber

Viewport-relative X (0..1) when there is no selector. Steers the real pointer too, so gliding into empty space lifts :hover off whatever was under it.

ynumber

Viewport-relative Y (0..1) when there is no selector.

durationMsnumberdefault 700

Cursor travel time (ms).

do: "click"Glide to an element and click it.
selectorstringrequired

Element to click.

durationMsnumberdefault 700

Cursor travel time to the element (ms).

do: "hover"Glide onto an element and rest, firing :hover. Follow with a wait to dwell.
selectorstringrequired

Element to hover.

durationMsnumberdefault 700

Cursor travel time to the element (ms).

do: "type"Type text into a field — the selector, or the already-focused field.
textstringrequired

Text to type.

selectorstring

Field to type into. Omit to act on the already-focused field.

delayMsnumberdefault 55

Ms between keystrokes (0 = instant). Humanized with mild jitter.

easingEasingdefault 'linear'

Eases the keystroke cadence across the run — "ease-in" starts quick and trails off, "ease-out" starts measured and quickens — without changing total time.

durationMsnumberdefault 700

Cursor travel time when a selector is given (the cursor glides to the field first).

do: "erase"Delete characters from a field — the selector, or the already-focused field.
selectorstring

Field to erase from. Omit to act on the already-focused field.

countnumber

How many characters to remove from the caret. Omit to erase the whole field.

delayMsnumberdefault 80

Ms between keystrokes (0 = instant). Humanized with mild jitter.

easingEasingdefault 'linear'

Eases the keystroke cadence across the run without changing total time.

durationMsnumberdefault 700

Cursor travel time when a selector is given.

do: "press"Press a key, optionally as a chord with modifiers.
keystringrequired

The key to press, e.g. "Enter", "Escape", "ArrowDown", or "f".

modifiers('Control' | 'Shift' | 'Alt' | 'Meta')[]

Modifier keys held during the press, e.g. ["Control"] for Ctrl+F.

do: "scrollTo"Scroll a target into view.
tonumber | stringrequired

Scroll target: a 0..1 number, an "NN%" string, or a CSS selector.

align'top' | 'center' | 'bottom'default 'top'

For a selector target: where it lands in the viewport.

offsetnumber

Px to nudge the resting position. Top-aligned, a positive offset leaves that many px of room above the target; negative scrolls past it.

speednumberdefault 400

Scroll speed in CSS px/second — the default pacing. The step runs for as long as the distance needs (distance ÷ speed), so every scroll moves at the same steady, human pace regardless of how far it travels.

durationMsnumber

A FIXED scroll time (ms) that overrides the default speed pacing — use it for a constant-time scroll, or durationMs: 0 for an instant jump (handy in setup). Omit to let speed pace it.

easingEasingdefault 'ease-in-out'

Shapes the scroll travel — "linear" holds a constant velocity (natural with speed), "ease-in-out" softens both ends.

do: "wait"Hold in place, doing nothing — the way to pause between steps.
durationMsnumberdefault 600

How long to wait (ms).

setupAction[]default []

Off-camera steps run before recording starts — pre-position the cursor, scroll, or set UI state so frame 0 is exactly where you want it. Trimmed from the output. Key to a seamless loop: place the cursor on the first target here with durationMs 0 so the clip doesn't open with a glide-in from center.

do'move' | 'click' | 'hover' | 'type' | 'erase' | 'press' | 'scrollTo' | 'wait'required

What this step does (same Action shape as actions).

teardownAction[]default []

Off-camera steps run after recording ends — reset the page to its opening state. Trimmed from the output. (For a visible return-to-start that closes the loop on screen, put those steps at the end of actions instead — teardown never appears in the clip.)

do'move' | 'click' | 'hover' | 'type' | 'erase' | 'press' | 'scrollTo' | 'wait'required

What this step does (same Action shape as actions).

cursorCursor

The synthetic cursor shown during the recording. Omit for the default cursor.

showboolean

Show the cursor. Defaults to true. Set false for hands-off UI-state clips (e.g. media wall tiles).

sizenumber

Cursor size (px). Defaults to 22.

colorstring

Cursor color. Defaults to white-with-shadow.

focusFocus

Element-focused clip: scroll one component into view, optionally trigger/dwell it, and crop the output to its box. The crop box is measured AFTER any actions run, so an expanded dropdown/drawer stays in frame. Omit for a full-viewport recording.

selectorstringrequired

Selector of the element to scroll into view and crop to.

paddingnumber

Padding (px) around the element when cropping. Defaults to 24.

actionsAction[]

Steps to trigger the component (e.g. open a dropdown) and/or dwell on it — end with a wait to hold on the element.

colorScheme'light' | 'dark'

Force a color scheme for the capture. Omit to leave as-is.

strictStepsbooleandefault false

Fail the asset when any step fails (missing selector, click timeout), instead of logging a warning and shipping the clip without that step.

{
  name: "menu-demo",
  url: "https://your-site.com",
  generator: "interaction",
  options: {
    // --- sizing & encoding ---
    output: {
      width: 1280,
      height: 800,
      deviceScaleFactor: 2,
      fps: 30,
      crf: 18,
      // fileName: "menu-demo.mp4",  // optional; defaults to <slug(name)>.mp4
    },

    // --- timing & navigation ---
    page: {
      startDelayMs: 500,
      endDwellMs: 800,
      waitUntil: "networkidle",
      // waitForSelector: "#hero",   // optional; wait for an element first
    },

    // --- the scripted tour (needs actions and/or focus) ---
    actions: [
      { do: "click", selector: "#menu-button" }, // durationMs 700 travel; add a { do: "wait" } to pause after
    ],
    // setup:    [], // off-camera steps before recording (pre-position cursor / scroll); trimmed out
    // teardown: [], // off-camera steps after recording (reset state); trimmed out
    // cursor: { show: true, size: 22, color: "#0b0b0f" }, // optional; default white-with-shadow

    // --- element-focused clip (alternative / addition to actions) ---
    // focus: { selector: "#pricing-card", padding: 24, actions: [{ do: "wait", durationMs: 2000 }] },

    // --- variant ---
    // colorScheme: "dark",          // optional; force light or dark
    // strictSteps: true,            // optional; fail the asset when a step fails (default: warn + continue)
  },
}

Every option also has hover docs in pro-visu.config.ts — the authoring types are generated from the validation schema, so the editor always matches what the tool accepts.

Examples

Scripted tour

actions scripts a live click-through with a visible cursor — UI in a state you could never screenshot from a static page. Clicks can navigate: the synthetic cursor survives page changes, so a tour can move across the whole site. This one loops the journey — start on the home page, open the shop and browse a module, hop to the About page for its hero, then click the wordmark home again and pin the scroll to the top, landing exactly where it began.

{name: "site-tour",url: "https://your-site.com",generator: "interaction",options: {  cursor: { color: "#8c7355" }, // the tinted synthetic cursor on screen  setup: [{ do: "move", selector: ".wordmark", durationMs: 0 }], // park where the tour ends  actions: [    { do: "click", selector: ".nav-inline a:nth-child(1)" }, // → /shop    { do: "wait", durationMs: 1300 },    { do: "scrollTo", to: "#edit" },                         // speed-paced (~400 px/s)    { do: "wait", durationMs: 700 },    { do: "click", selector: ".edit-thumb:nth-child(2)" },    { do: "wait", durationMs: 900 },    { do: "click", selector: ".edit-thumb:nth-child(3)" },    { do: "wait", durationMs: 900 },    { do: "click", selector: ".edit-thumb:nth-child(4)" },    { do: "wait", durationMs: 1000 },    { do: "click", selector: ".nav-inline a:nth-child(3)" }, // → /about (hero)    { do: "wait", durationMs: 2400 },    { do: "click", selector: ".wordmark" },                  // → home    { do: "wait", durationMs: 400 },    { do: "scrollTo", to: 0, durationMs: 0 },                // instant pin to the top → loops    { do: "wait", durationMs: 1400 },  ],},}
A multi-page tour, looped: home → shop (browse The Edit) → About hero → back home, one continuous take that cuts cleanly into its own start.

Seamless loop

An interaction can't loop automatically — the tool doesn't know what your clicks did to the UI — but setup and teardown give you the two off-camera windows to build one yourself. Both run the same Action steps as actions, but neither appears in the output: setup is folded into the head trim (before recording), teardown runs past the tail (after recording), so both are cut from the clip.

For a loop the last frame must match the first. Two moves get you there:

  1. setup pre-positions the cursor on the first target (durationMs: 0), so frame 0 opens with the cursor already in place instead of gliding in from the center — cleaner even for non-looping clips.
  2. The last on-camera steps return to that opening state — travel back to the first element and undo whatever the tour changed (reselect the default, close the menu or drawer). This is what actually closes the loop; teardown is only for off-camera cleanup, since it never shows.

Below, a size selector cropped with focus: setup selects XS off-camera so the clip opens on the first size, the tour works up S → M → L → XL, and the final tap returns to XS — so the last frame (XS selected, cursor resting on it) is identical to the first. Cropping to .addbag keeps the button below the sizes in frame, so the whole component reads as one unit.

{name: "size-loop",url: "https://your-site.com/products/the-camel-coat",generator: "interaction",options: {  cursor: { color: "#8c7355" }, // the tinted synthetic cursor on screen  // off-camera: select XS so the loop opens on the first size  setup: [{ do: "click", selector: ".size-options button:nth-of-type(1)", durationMs: 0 }],  focus: {    selector: ".addbag",    padding: 24,    actions: [      { do: "click", selector: ".size-options button:nth-of-type(2)" }, // S      { do: "wait", durationMs: 600 },      { do: "click", selector: ".size-options button:nth-of-type(3)" }, // M      { do: "wait", durationMs: 600 },      { do: "click", selector: ".size-options button:nth-of-type(4)" }, // L      { do: "wait", durationMs: 600 },      { do: "click", selector: ".size-options button:nth-of-type(5)" }, // XL      { do: "wait", durationMs: 600 },      { do: "click", selector: ".size-options button:nth-of-type(1)" }, // back to XS → loops      { do: "wait", durationMs: 900 },                                  // dwell on the element before the cut    ],  },},}
A self-contained component loop: start on XS, work up to XL, then land back on XS — the clip cuts seamlessly into its own start.

A clean loop is realistic mainly for click / hover / scroll tours whose state you can fully restore. Autoplaying video, running CSS animations, or late-loading content won't line up frame-to-frame.

Typing, editing & keys

Three actions drive form input, and they read as a real person at the keyboard:

  • type enters text a key at a time. delayMs sets the per-keystroke pace (default 55ms) and easing shapes the cadence across the word — ease-in starts quick and trails off, ease-out starts measured and quickens — without changing the total time. A little jitter is always mixed in so it never taps like a metronome.
  • erase removes characters. Omit count to clear the whole field, or set it to edit just the tail (count: 6). It backspaces at delayMs (default 80ms) so the deletion is visible; delayMs: 0 blanks it instantly.
  • press sends one key (key: "Enter"), with optional modifiers for chords ({ key: "f", modifiers: ["Control"] } → Ctrl+F).

Below, a seamless loop over a search that filters on submit (Enter). setup lands the camera on the search field (top-aligned, 40px below the sticky header). Search "trousers" → Enter → glance at the result; then edit the query — erase, type "knitwear" → Enter → glance across the two results and open the second; dwell on the product, then navigate back to the list so the last frame matches the first.

{name: "search-demo",url: "https://your-site.com/shop",generator: "interaction",options: {  cursor: { color: "#8c7355" }, // the tinted synthetic cursor on screen  page: { waitForSelector: "#search-input", stickyHeaderHeight: 100 }, // header height → scrollTo lands below it  setup: [    { do: "scrollTo", to: "#search-input", align: "top", offset: 40, durationMs: 0 }, // start on the field (40px below the header)    { do: "move", selector: "#search-input", durationMs: 0 },                          // park the cursor (frame 0)  ],  actions: [    { do: "click", selector: "#search-input" },    { do: "wait",  durationMs: 400 },    { do: "type",  selector: "#search-input", text: "trousers", delayMs: 55, easing: "ease-out" },    { do: "wait",  durationMs: 300 },    { do: "press", key: "Enter" },                                     // submit → the list updates    { do: "wait",  durationMs: 800 },    { do: "hover", selector: ".product:first-child .product-media" },  // glance at the result    { do: "wait",  durationMs: 1000 },    { do: "erase", selector: "#search-input", delayMs: 80, easing: "ease-in" }, // clear the field    { do: "wait",  durationMs: 300 },    { do: "type",  selector: "#search-input", text: "knitwear", delayMs: 55 },   // edit the query    { do: "wait",  durationMs: 300 },    { do: "press", key: "Enter" },                                     // submit → two results    { do: "wait",  durationMs: 800 },    { do: "hover", selector: ".product:first-child .product-media" },  // ~1s on the first    { do: "wait",  durationMs: 1000 },    { do: "hover", selector: ".product:nth-child(2) .product-media" }, // ~1s on the second    { do: "wait",  durationMs: 1000 },    { do: "click", selector: ".product:nth-child(2) .product-link" },  // open the second    { do: "wait",  durationMs: 2000 },                                 // dwell 2s    { do: "click", selector: ".nav-inline a:nth-child(1)" },           // → back to the list    { do: "wait",  durationMs: 900 },    { do: "scrollTo", to: "#search-input", align: "top", offset: 40, durationMs: 800 }, // fixed-time glide to the start    { do: "wait",  durationMs: 500 },    { do: "move", selector: "#search-input", durationMs: 600 },        // cursor back on the field → loops    { do: "wait",  durationMs: 1000 },  ],},}
A search loop: submit a query with Enter, glance at the result, edit the query and submit again, open a result, then return to the list where it began — hand-driven typing and backspacing, and the camera set once via setup.

The cursor never scrolls the page. Moving, hovering, clicking or typing on a target assumes it's already in view — the viewport is a camera you drive only with scrollTo (an eased, explicit move). Here setup frames the search field once (align: "top", offset: 40), and because its results sit just below, nothing needs to scroll again until the loop returns to that same start. Set page.stickyHeaderHeight to your header's height and top-aligned scrolls land below it — offset then stacks on top (here, 40px under the header). If an action's target is off-screen, the tool warns you to add a scrollTo rather than silently hard-jumping to it.

Scrolls are paced by distance, not a fixed time. A scrollTo runs at speed (CSS px/second, default 400) for as long as the distance needs — distance ÷ speed — so every scroll moves at the same steady, human pace whether it travels a whole page or a short hop. Raise or lower speed to taste; pair it with easing: "linear" for a true constant velocity (the default ease-in-out softens both ends, peaking ~2× mid-scroll). For a fixed-time scroll instead, set durationMs — and durationMs: 0 jumps instantly, which is how setup pins the camera without an on-screen scroll.

{ do: "scrollTo", to: 1, speed: 1000, easing: "linear" } // to the bottom at a steady 1000 px/s
{ do: "scrollTo", to: 0, durationMs: 0 }                 // instant jump to the top (e.g. in setup)

Element focus

focus scrolls one component into view, optionally triggers and dwells on it (end focus.actions with a wait), and crops the output to its box — the crop is measured after the actions, so an expanded state stays in frame. This one opens at rest with the cursor parked just off the card (a selector-less move steers the real pointer too, so nothing is hovered). The cursor glides on — the controls reveal and the image zooms — the wishlist fills, the button confirms Added to bag, the heart clears, then the cursor glides back off the card, which lifts the hover and settles it to rest again: an exact match for the opening frame.

{name: "card-demo",url: "https://your-site.com/shop",generator: "interaction",options: {  cursor: { color: "#8c7355" }, // the tinted synthetic cursor on screen  setup: [{ do: "move", x: 0.16, y: 0.87, durationMs: 0 }], // rest: cursor just off the card  focus: {    selector: "#feature-card",    padding: 26,    actions: [      { do: "hover", selector: "#feature-card .product-media" }, // reveal + zoom      { do: "wait", durationMs: 900 },      { do: "click", selector: "#feature-card .wishlist" },      // heart fills      { do: "wait", durationMs: 1000 },      { do: "click", selector: "#feature-card .quick-add" },     // → "Added to bag"      { do: "wait", durationMs: 1300 },      { do: "click", selector: "#feature-card .wishlist" },      // heart clears      { do: "wait", durationMs: 700 },      { do: "move", x: 0.16, y: 0.87, durationMs: 600 },         // glide off → card resets → frame 0      { do: "wait", durationMs: 1100 },    ],  },},}
Cropped to one product card, looped: glide in to reveal the controls, fill the wishlist, add to bag, then glide off — the hover lifts and the card resets to where it started.

On this page