Recording Flows
ScreenCI turns Playwright scripts into product videos. If you’ve written a Playwright test before, you already know most of what you need.
Quick start
1. Install
npm install screenci2. Init a project
npx screenci init my-projectcd my-projectnpm installThis creates:
my-project/ screenci.config.ts ← recording settings videos/ example.video.ts ← starter script Dockerfile ← for CI recording in a container .gitignore package.json3. Write a video script
Video scripts are plain .video.ts files. Each video() call produces one recording:
import { video } from 'screenci'
video('Onboarding flow', async ({ page }) => { await page.goto('https://app.example.com/signup') await page.fill('input[name="name"]', 'Jane Doe') await page.click('button[type="submit"]') await page.waitForURL('**/dashboard')})That’s Playwright. screenci extends it — it does not replace it.
4. Develop without recording
npm run dev# or: npx screenci devOpens the Playwright UI. Run your scripts, verify they work, fix selectors — no screen capture, no container, no FFmpeg. Just normal Playwright test execution.
5. Record
cd my-project && npm run record# or: npx screenci recordLaunches a headless browser in a virtual display, runs FFmpeg to capture the screen, and saves:
.screenci/ onboarding-flow/ recording.mp4 data.jsonWhat ScreenCI adds
Everything in Playwright’s page API works unchanged. On top of that, screenci gives you:
| Feature | What it does |
|---|---|
| Animated cursor paths | Clicks arrive with a smooth bezier curve instead of teleporting |
| Typed character input | fill() types character-by-character so the viewer sees keystrokes |
hide(fn) | Cuts a section from the final video (logins, page loads, setup) |
autoZoom(fn) | Smooth camera pan that follows interactions inside the callback |
createCaptions() | AI voiceover markers that sync to the recording at render time |
createAssets() | Image or video overlays shown during the recording |
These are composable. You can combine hide, autoZoom, and captions around any Playwright code.
A complete example
import { video, hide, autoZoom, createCaptions } from 'screenci'
const captions = createCaptions({ openForm: "Let's add a new team member.", submit: "One click and they're in.",})
video('Invite a team member', async ({ page }) => { // Login happens off-screen — viewer jumps straight to the app await hide(async () => { await page.goto('/login') await page.fill('input[type="password"]', 'secret') await page.click('[type="submit"]') await page.waitForURL('**/dashboard') })
// Camera follows the invite form await captions.openForm.start() await autoZoom( async () => { await page.locator('#invite').click() }, { duration: 400, easing: 'ease-in-out', amount: 0.4 } ) await captions.openForm.end()
await captions.submit.start() await autoZoom( async () => { await page.locator('button[type="submit"]').click() await page.waitForTimeout(500) }, { duration: 400, easing: 'ease-in-out', amount: 0.4 } ) await captions.submit.end()})Tips
Hide navigation at the start
The very first thing your video does is almost always a page.goto(). Wrap it in hide() so the video jumps straight into the live, ready UI:
video('CRM demo', async ({ page }) => { await hide(async () => { await page.goto('https://app.example.com/') await page.waitForLoadState('networkidle') await page.waitForTimeout(2000) })
// Viewer sees the app fully loaded await page.locator('#new-deal').click()})Hide navigation between sections too
Use hide() between page transitions as well — viewers don’t need to watch a loading spinner:
// ... previous section ...
await hide(async () => { await page.locator('nav a[href="/reports"]').click() await page.waitForURL('**/reports')})
await autoZoom( async () => { // interact with the reports page }, { duration: 400, easing: 'ease-in-out', amount: 0.4 })One autoZoom per page section
Wrap a full form or page section in one autoZoom, not one per click. The camera zooms in when you arrive and zooms back out when you leave — a single, smooth motion rather than a series of jolts.
Test titles become filenames
video('My Feature Demo') outputs to .screenci/my-feature-demo/. Keep titles unique within a project after kebab-case normalization.
Other recording methods
Browser extension
For non-technical team members who want to record without writing scripts. Point and click through a flow and the extension writes the .video.ts file for you.
MCP server
Use the screenci MCP server with AI editors (Cursor, Claude Desktop, etc.) to generate or edit video scripts with natural language. Ask your assistant to “update the recording to use the new button label” and it handles the script surgery.