π¬βRecording a web app
How do I put a demo of my product in a deck or on a landing page without shipping a video of it?
A product demo in a deck is almost always an MP4, and an MP4 is the wrong format for a user interface. It is lossy on exactly the content it carries β small type, hairlines, flat colour β it is a fixed grid of pixels in a layout that is not, and it weighs megabytes a minute.
narro record captures the sources of the frame instead: the DOM as it stood,
every mutation and input that followed, and the bytes of everything the page
drew with. The result replays as real DOM.
npx @getnarro/cli record https://your.app --html demo.html
A window opens, you drive the flow, and pressing Enter writes the file. Open it and the recording plays: text you can select, sharp at any zoom, and the whole thing works offline.
Stopping it
The recording ends when you say so, and there are three ways to say it. All three save:
| Do this | And |
|---|---|
| press Enter in the terminal | the browser closes and the file is written |
| press Ctrl+C in the terminal | the same thing happens. It is not an abort |
| close the browser window | the same thing happens |
The terminal says so while it is recording, so there is nothing to remember:
β recording β drive the flow in the window
press Enter here to stop and save
Ctrl+C or closing the window save it too β nothing is lost
Only a second Ctrl+C, after the first one has been acknowledged, abandons the run β by then the file is usually already written.
Recording the browser you already have
The window record opens is a browser with a fresh profile: signed out of
everything, no extensions, none of the state a demo of a real product is made
of. --connect records the Chrome you are already using instead.
Start Chrome once with remote debugging on:
# macOS
open -na "Google Chrome" --args --remote-debugging-port=9222 --user-data-dir="$HOME/.narro-chrome"
# Linux
google-chrome --remote-debugging-port=9222 --user-data-dir="$HOME/.narro-chrome"
# Windows
chrome.exe --remote-debugging-port=9222 --user-data-dir="%TEMP%\narro-chrome"
Then record it:
npx @getnarro/cli record https://your.app --connect --html demo.html
--connect on its own means http://127.0.0.1:9222. A port, a host and port,
or a full URL all work if you moved it: --connect 9333.
Two things worth knowing about that Chrome flag, because neither is guessable:
- The separate
--user-data-diris not optional. Since Chrome 136 the default profile ignores--remote-debugging-portβ a security fix β so a Chrome started without it accepts no connection and--connectreports that nothing is listening. The directory you name is a real profile that keeps its cookies: sign in once and every later recording is already signed in. - It has to be started with the flag. A Chrome that is already open cannot be given the port afterwards.
What attaching does, once connected:
- If a tab is already showing the URL, that tab is recorded where it stands β no reload, so the half-filled form or the state you spent five minutes getting into is still there when recording starts. rrweb opens with a full snapshot of whatever is on screen, so a recording can begin in the middle of a session.
- Otherwise a new tab opens on the URL. Your other tabs are not touched.
- When the recording stops, narro disconnects. Your browser, and every tab in it, keeps running.
- The recording viewport is whatever that window is, so
--sizedoes not apply. Resize the window before you start.
--connect and --auto are opposites β one records the browser you are
driving, the other tours a page in a browser of its own β so the two cannot be
combined.
The window it opens, and its size
--size is the size of the page, and the window is sized to match: a
--size 1440x900 recording gets a window with 1440x900 of page in it, plus
whatever height the tab strip and omnibox need on your platform.
npx @getnarro/cli record https://your.app --size 1280x800
If the screen cannot fit that β 1440x900 on a 1366x768 laptop wants a window with its foot below the display β the window is shrunk to fit and the terminal says what it recorded instead:
1366x681
(1440x900 would not fit on this screen)
The number printed is measured from the window rather than assumed, and it is what the cast records as its viewport. That matters more than it sounds: a cast is not responsive, and the replay scales the layout it captured, so a cast whose stated viewport was not the one the page laid itself out at replays at the wrong shape.
Two ways to embed it
They want different files, so ask for the one you need.
A website should serve the parts separately, because a site already knows how to serve and cache files:
npx @getnarro/cli record https://your.app --embed ./public/demo
That writes the player, the cast, and the assets as ordinary files, plus the snippet to paste:
<script src="/demo/narro-player.global.js"></script>
<narro-replay src="/demo/recording.narrocast" controls autoplay cursor="trail"></narro-replay>
The player is the same bytes for every demo on the site, so it caches once. The page itself pays for the event stream β tens of kilobytes for a short flow.
Beside those files it also writes EMBEDDING.md β the same demo written out for
React, Astro, Preact, Vue and Svelte, with your own paths already in it β and
PROMPT.md, which is that written as an instruction to hand a coding agent.
record offers you the same prompt when it finishes, so the next step is one
keystroke away rather than a file you have to go and find.
Embedding a replay is the full version.
Anything you hand someone β an email, a deck that must work on a plane, a
file on a stick β wants --html: one file, no server, no network.
On a slide
In a markdown deck, a fence β no import, no project:
```replay
src: /demo/recording.narrocast
poster: /demo.png
cursor: trail
```
Put the recording in the deckβs public/ directory, exactly as you would an
image. Every option is listed under
Replays.
In a React deck β or any React app β the typed wrapper lets the page drive the recording rather than the viewer, and keeps the 200 KB player out of the bundle until a replay is on screen:
import { Replay } from "@getnarro/replay/react";
<Replay src="/demo.narrocast" chapter={step === 2 ? "publish" : "open the editor"} />
Because narro export --pdf and --pptx produce files that cannot run
JavaScript, record a still for them at the same time:
npx @getnarro/cli record https://your.app --html demo.html --poster demo.png
The cursor
A replay with no cursor is a page that changes for no visible reason β the
viewer sees the result of a click and never the click. --cursor pointer draws
one; --cursor trail also draws the path it took, which is what a flow through
several screens usually needs; --cursor none hides it.
Unattended recordings (--auto 8) move the pointer as well as scrolling, for
the same reason. They scroll whatever the page actually scrolls: an app shell β
a full-height layout whose main pane moves while the document does not β is
toured by its pane, so a tour of one is not a still frame with a cursor on it.
Selecting the text in a replay
A reader can drag across the words in a replay and highlight them, which is the one thing no video of an interface can offer, and worth pointing at in the caption. Three things follow from a live frame, and the player handles all three: a click cannot navigate away from the recording, the wheel scrolls the page around the demo rather than the demo, and a selection holds the replay where it is β text that scrolls out from under the pointer cannot be selected β until nothing is selected again. On a touch screen the frame stays inert, so a thumb pans the page.
What a cast is not
- Not responsive. Layout was computed once, at the recording viewport, and
every scroll offset in the stream assumes it. The player scales rather than
reflows. Record a second cast at
--size 390x844for a phone. - Not the application. The recording carries the DOM your app produced, never its code β nothing runs on replay, so anything that was not recorded does not exist. Interactivity is a re-enactment, not a live product.
- Blind to canvas and WebGL. A chart library, a map or a 3D viewer records as an empty element. Those demos still want a video.
Before you record something real
A cast is text, and grep works on it. It carries what was on screen and a
great deal that was not: content behind display: none, options in a select
nobody opened, everything below the fold, and β the part that surprises people β
every attribute and URL, including title, alt, aria-label, data-* and
any token sitting in an href.
--mask applies rrwebβs privacy profile, which masks input values and drops
scripts and comments. It is worth using and it is not sufficient: it does
not touch attributes or URLs at any setting. Recording a page of real customer
data is a decision to publish that data, so record a demo account.
Why it is small
Measured on a real page, the portable single file is 414 KB where the obvious implementation produces 1.5 MB, and a website pays 60 KB for the cast:
- the player carries rrwebβs replay half only β a page showing a recording has no reason to ship a recorder;
- assets are stored once, under the hash of their bytes, rather than inlined
wherever they appear, which otherwise ships the same font twice β and that
includes images the page had already inlined as
data:URIs, worth about a fifth of a cast on a page that repeats one; - images are re-encoded to WebP at the same dimensions;
- and both halves travel gzipped, at the most zlib will give. Not brotli, which
would be worth twice as much and which no browser will decompress from
DecompressionStreamβ see how big it is.
Fonts are the one thing a cast must carry rather than link. A font file is
fetched in CORS mode no matter whose stylesheet asked for it, and a self-hosted
face ships no Access-Control-Allow-Origin header β because same-origin never
needed one. On any other origin, every one of them is refused.