- Drop the sync command, preview --watch/--no-watch/--no-sync, --record-kill-window, and the queued-edit drains before test/export/preview. Editor edits now live in the web editor and are not written back to sources. - Delete codeSync, applyCodegen, editorOptionsSync, devWatch, codegenFailureLog and their specs; trim devListen to register/sync-state/deregister. - preview is always one-shot; generated CI workflows run plain screenci preview. - CLI links point at the video overview page: preview prints /project/<pid>/video/<vid>, a single-video export prints ...?export=<recordId>, multi-video runs keep the run pages. Held-video notices deep-link ?editor. - Rewrite docs, skill files, and the narration guidance: narrate the flow rather than each click, use the product's own vocabulary, and fill forms with fictitious example data (Emma Carter, emma@aperturebio.com).
screenci
Your UI changed. Your demo videos didn't. screenci fixes that.
Record product walkthroughs as code. When the UI ships, run
npx screenci export and your videos regenerate. You keep the Playwright
mental model, but the output is a maintainable product video instead of a
test report.
Learn more at screenci.com.
Get started
npm init screenci@latest
# or
pnpm create screenci
This scaffolds a self-contained screenci/ directory with its own
dependencies and installs Chromium. The directory is isolated from the
surrounding workspace, which keeps installation reliable inside monorepos.
Then write a video, run it locally, refine it, and export the final output:
npx screenci test # author the video
npx screenci preview # record live previews and print the link
npx screenci export # render and download the finished video
Full docs:
Write a video
Video scripts are Playwright-style files with a .screenci.ts extension. If you
already know Playwright locators, navigation, and waiting, you already know
most of the automation layer.
// recordings/onboarding.screenci.ts
import { hide, speed, time, video } from 'screenci'
video('Onboarding flow', async ({ page }) => {
await hide(async () => {
await page.goto('https://app.example.com/signup')
})
await page.getByLabel('Email').fill('jane@example.com')
await page.getByRole('button', { name: 'Create account' }).click()
await speed(0.5, async () => {
await page.getByRole('button', { name: 'Open dashboard tour' }).click()
})
await time(1000, async () => {
await page.getByRole('button', { name: 'Skip tutorial' }).click()
})
await page.getByRole('heading', { name: 'Dashboard' }).waitFor()
})
Each video() call becomes one output video. The title becomes the filename
and the remote video identity. Inside video(), page is a ScreenCIPage: a
Playwright Page with animated cursor movement and visible typing layered on
top of normal Playwright behavior.
hide() removes setup entirely. speed() and time() keep a section visible
but remap its rendered duration.
Authoring helpers
| Export | What it does |
|---|---|
defineConfig |
Wraps Playwright config with ScreenCI defaults |
video |
Declares a video recording test |
video.narration |
Declares narration cues (per language or shared) for a video |
hide |
Cuts setup or cleanup out of the visible recording |
autoZoom |
Smooth camera follow for an interaction block |
zoomTo |
Manual camera framing for a locator or point |
resetZoom |
Returns from manual framing to the full viewport |
createOverlays |
Inserts timed media overlays into the recording timeline |
voices |
Available voice constants such as voices.Ava |
modelTypes |
Narration model constants |
See the docs for configuration, narration, camera, and CI setup.
Community
Questions, ideas, or want to show off your videos? Join us on Discord.
.screenci.ts files and running the Screenci workflow.