Tutorial videos of your app, from one prompt.

Describe a feature and paste the prompt into your coding agent. It records a narrated walkthrough of your real app, ready for your help center, docs, social media, or the product itself.

Works with any agent, such as

Free to try, no account needed.

1

Describe the video

No app to record yet?(fills in demo.screenci.com and a video to make there).

2

Paste into Claude Code, Codex, or Cursor

Any coding agent that runs shell commands works. It uses ScreenCI to record your real app on your own machine.

Tip: for the smoothest experience, use a fast model with low thinking effort. The agent mostly runs commands, so a big, slow model only adds waiting.

> Integrate ScreenCI by fetching https://screenci.com/integrate.md and following its steps, then add a screenci video about '<feature>'  Installing screenci  done  Writing recordings/first-video.screenci.ts  done  Recording on this machine  1 take, 42s  Preview ready  opening in your browser
3

Watch it in your browser

Your tab opens the finished video. To change it, prompt the agent again with what should be different.

Prefer the terminal? Run npm init screenci@latest (manual setup). Or read the agent brief first.

Real renders, no mockups

What comes back

Every clip here is a real ScreenCI render of a real app, made from one prompt. The camera, the narration, the subtitles, the branding, and the languages come with every tutorial.

The camera follows the flow for you

Auto-zoom tracks every click, field, and drag.

Show the generated e2e test file
The camera follows the flow for you

The camera zooms in and pans across the screen to follow each interaction: typing a field, a segmented control, a slider drag, a dropdown pick. No need to set manual zoom points, unless you want to be explicit.

import { autoZoom, hide, resetZoom, video } from 'screenci'

// Auto-zoom follows the action: the camera zooms and pans to whatever you touch,
// with no manual zoom points. The "Add a contact" form keeps its controls on two
// side-by-side cards (contact details left, plan and access right), so this one
// block zig-zags corner to corner and the camera makes big jumps between each
// interaction.

video('Auto-zoom', async ({ page }) => {
  await hide(async () => {
    await page.goto('https://demo.screenci.com/customers/new')
    await page.waitForLoadState('networkidle')
  })

  // A tight `amount` frames each control closely, so the camera makes a big pan
  // across the screen to follow each next interaction.
  await autoZoom(
    async () => {
      // Top-left: type into the work email field.
      await page
        .getByLabel('Work email')
        .pressSequentially('[email protected]')

      // Jump top-right: pick a plan from the segmented control.
      await page.getByRole('button', { name: 'Scale' }).click()

      // Jump back left: type into the full-name field.
      await page.getByLabel('Full name').pressSequentially('Emma Carter')

      // Jump right: drag the seat-count slider up to ~62 of 100. dragTo targets a
      // point along the slider root, and the camera follows the thumb across it.
      const thumb = page.locator('[data-slot="slider-thumb"]').first()
      const track = thumb.locator('xpath=ancestor::*[@data-slot="slider"][1]')
      const trackBox = (await track.boundingBox())!
      await thumb.dragTo(track, {
        targetPosition: {
          x: (trackBox.width * (62 - 1)) / (100 - 1),
          y: trackBox.height / 2,
        },
        moveDuration: 900,
        dragDuration: 1000,
        dragSteps: 32,
      })

      // Bottom corners: open a dropdown on each side and pick.
      await page.getByRole('combobox', { name: 'Account type' }).click()
      await page.getByRole('option', { name: 'Customer' }).click()
      await page.getByRole('combobox', { name: 'Region' }).click()
      await page.getByRole('option', { name: 'Asia Pacific' }).click()
    },
    { amount: 0.45 }
  )
  await resetZoom()
})

Drop in images and video

Your own assets, timed on the timeline.

Show the generated e2e test file
Drop in images and video

Place PNG/SVG images and MP4 clips as timed file overlays. This is different from programmatic overlays: the media is a real asset file, not a React or HTML overlay.

import { hide, video } from 'screenci'

// Embed media files directly: images/SVGs and MP4 clips are assets on the
// timeline. Programmatic overlays are React/HTML components; these are files.
video.overlays({
  logo: { path: './assets/logo.png', fill: 'recording', duration: 2100 },
  preview: {
    path: './assets/1-sample-code.mp4',
    fill: 'recording',
    volume: 0,
    time: 3200,
  },
})('Embed media', async ({ page, overlays }) => {
  await hide(async () => {
    await page.goto('https://demo.screenci.com/analytics')
    await page.waitForLoadState('networkidle')
  })

  await overlays.logo()
  await page.waitForTimeout(900)
  await overlays.preview()
  await page.waitForTimeout(1400)
})

Docs screenshots that update themselves

Framed stills from the same script.

Show the generated e2e test file
Docs screenshots that update themselves

Capture any page as a framed image with screenshot(), dressed with code overlays and auto-updating on every re-render.

import { screenshot } from 'screenci'

const BACKGROUND_CSS =
  'radial-gradient(circle at top left, #dbeafe 0%, #eff6ff 28%, #f8fafc 58%, #e0f2fe 100%)'

// A framed still of a live page, dressed with two code-built overlays (a status
// pill and a lower-third caption). Because it comes from the same script as the
// videos, it re-renders and stays current on every run, no manual re-capture.
const shot = screenshot.overlays({
  // A React lower-third with props.
  caption: {
    path: './overlays/Callout.tsx',
    props: { title: 'Framed, branded, always current' },
    x: 300,
    y: 640,
    width: 640,
  },
  // A plain HTML status pill.
  statusPill: { path: './overlays/status-pill.html', x: 1000, y: 700, width: 420 },
})

screenshot.use({
  renderOptions: {
    screenshot: { aspectRatio: '16:9', margin: 96, mouse: { show: false } },
    recording: {
      roundness: 0.05,
      dropShadow: 'drop-shadow(0 22px 49px rgba(0,0,0,0.28))',
    },
    output: {
      background: { backgroundCss: BACKGROUND_CSS },
    },
  },
})

shot('Branded screenshot', async ({ page, overlays }) => {
  await page.goto('https://demo.screenci.com/sales/d3')
  await page.getByRole('heading', { name: 'Annual renewal and expansion' }).waitFor()

  // A still has no timeline: start each overlay and leave it open so it stays
  // baked into the captured image.
  await overlays.caption.start()
  await overlays.statusPill.start()
})

Narrated in 79 languages

Every line here is in a different language.

Show the generated e2e test file
Narrated in 79 languages

One recording, with each narration line spoken in a different language via a per-cue language override. AI voices and subtitles, all from the same script.

import { hide, video } from 'screenci'

// One recording, every language: each cue overrides its own `language`, so a
// single render greets you in a different tongue on every line.
video.narration({
  hello: { cue: 'Hello from ScreenCI.', language: 'en' },
  spanish: { cue: '¡Compatible con el español!', language: 'es' },
  french: { cue: 'Compatible avec le français !', language: 'fr' },
  german: { cue: 'Unterstützt Deutsch!', language: 'de' },
  chinese: { cue: '支持中文!', language: 'cmn' },
  japanese: { cue: '日本語に対応しています!', language: 'ja' },
  hindi: { cue: 'हिंदी भी चलती है!', language: 'hi' },
})('Multilanguage narration', async ({ page, narration }) => {
  await hide(async () => {
    await page.goto('https://demo.screenci.com/')
    await page.waitForLoadState('networkidle')
  })

  await narration.hello()
  await narration.spanish()
  await narration.french()
  await narration.german()
  await narration.chinese()
  await narration.japanese()
  await narration.hindi()
})

Every market gets its own version

One recording, localized for each language.

Show the generated e2e test file
Every market gets its own version

Record the page once and render it in every language you list, each with its own AI narration and subtitles. Update the source and all languages re-render in sync.

import { hide, video } from 'screenci'

// Localize the videos: record the /welcome page once, and ScreenCI renders it in
// every language you list, each with its own narration. `browserLocale` (on by
// default) sets the browser locale per render, so the page localizes itself and
// the narration matches.

// Fill the whole output frame (no gradient margin) so the localized content is
// full-bleed, and stays that way when embedded by the compose showcase. Keeps
// the shared marketing background + framing, but overrides size to 1 (full
// bleed) so this clip has no gradient margin.
video
  .renderOptions({
    output: {
      background: {
        backgroundCss:
          'radial-gradient(circle at top left, #dbeafe 0%, #eff6ff 28%, #f8fafc 58%, #e0f2fe 100%)',
      },
    },
    recording: { size: 1, roundness: 0.04, dropShadow: 1 },
  })
  // The target: the localized /welcome page, recorded in en/es/fi.
  .narration({
    en: { intro: 'Welcome to Northstar. Let’s set up your workspace.' },
    es: { intro: 'Bienvenido a Northstar. Vamos a configurar tu espacio.' },
    fi: { intro: 'Tervetuloa Northstariin. Otetaan työtila käyttöön.' },
  })('Localized welcome', async ({ page, narration }) => {
  await hide(async () => {
    await page.goto('https://demo.screenci.com/welcome')
    await page.waitForLoadState('networkidle')
  })

  await narration.intro()
  await page.waitForTimeout(800)
})

Customer data never enters the recording

Secrets are masked before the frame is captured.

Show the generated e2e test file
Customer data never enters the recording

Redact API keys, tokens, and PII with redact(). The pixels are masked in the page before the frame is captured, so the secret never enters the recording that gets uploaded. Not a cosmetic overlay: true redaction.

import { hide, resetZoom, video, zoomTo } from 'screenci'

// True redaction: the mask is applied in the browser before any frame is
// captured, so the secret never enters the uploaded recording.
video('Redact secrets', async ({ page }) => {
  const visibleSecret = 'sk_live_51ExistingValue8f1c2a'
  const redactedSecret = 'sk_live_51P0J8vX2f7r9d3cH'

  await hide(async () => {
    await page.goto('https://demo.screenci.com/settings/company')
    await page.waitForLoadState('networkidle')
    await page.getByLabel('Tax ID').fill(visibleSecret)
  })

  const taxId = page.getByLabel('Tax ID')
  await zoomTo(taxId, { amount: 0.5, duration: 300 })
  await page.waitForTimeout(700)
  await taxId.fill(redactedSecret, {
    duration: 900,
    redact: true,
  })
  await resetZoom()
})

On brand by default

Frame every clip on your branded background.

Show the generated e2e test file
On brand by default

Pick a render background in the web editor to frame every recording on a branded gradient with rounded corners and a soft shadow.

import { hide, video } from 'screenci'

// Frame every recording on your own brand background, with size, corner
// roundness, and shadow set once in code (or with the same controls in the
// editor).
video
  .renderOptions({
    output: {
      background: {
        backgroundCss:
          'linear-gradient(135deg, oklch(0.62 0.17 268), oklch(0.4 0.18 275))',
      },
    },
    recording: { size: 0.82, roundness: 0.06, dropShadow: 1 },
  })
  .overlays({
    caption: { path: './overlays/Callout.tsx', x: 72, y: 760, width: 640 },
  })('Styled backgrounds', async ({ page, overlays }) => {
  await hide(() => page.goto('https://demo.screenci.com/'))

  await overlays.caption.start()
  await page.getByRole('button', { name: 'Pipeline', exact: true }).click()
  await page.locator('[data-deal-card]', { hasText: 'Vega Logistics' }).click()
  await overlays.caption.end()
})

The boring steps go fast

Speed ramps skip the repetitive parts.

Show the generated e2e test file
The boring steps go fast

Fast-forward the repetitive steps with speed() so every walkthrough stays tight.

import { hide, resetZoom, speed, time, video, zoomTo } from 'screenci'

// Speed ramps two ways. Explicit `speed` or `time`, both work:
//   speed(n): a RELATIVE multiplier on playback. n > 1 fast-forwards, n < 1 slows
//             down. Best for compressing (or stretching) repetitive stretches.
//   time(ms): an ABSOLUTE target. The block is retimed to occupy exactly this
//             long, however long it really took, so you can pin one beat to a
//             precise duration.
// Shared marketing render styling (project-wide look): light radial-gradient
// background, framed recording tile with soft corners and a drop shadow.
video.renderOptions({
  output: {
    background: {
      backgroundCss:
        'radial-gradient(circle at top left, #dbeafe 0%, #eff6ff 28%, #f8fafc 58%, #e0f2fe 100%)',
    },
  },
  recording: { size: 0.9, roundness: 0.04, dropShadow: 1 },
})('Speed ramps', async ({ page }) => {
  await hide(async () => {
    await page.goto('https://demo.screenci.com/settings/company')
    await page.waitForLoadState('networkidle')
  })

  await zoomTo(page.locator('form').first())

  // speed(): fast-forward the repetitive fields into a quick montage (3x).
  await speed(3, async () => {
    await page
      .getByLabel('Legal name')
      .fill('Aperture Bio Inc', { editId: 'fill1' })
    await page.getByLabel('Tax ID').fill('47-2819001', { editId: 'fill11' })
    await page.getByLabel('Phone').fill('(512) 555-0142', { editId: 'fill10' })
    await page
      .getByLabel('Address')
      .fill('2200 Congress Ave', { editId: 'fill9' })
    await page.getByLabel('City').fill('Austin', { editId: 'fill2' })
    await page.getByLabel('Postal code').fill('78701', { editId: 'fill3' })
    await page.getByLabel('Country').fill('United States', { editId: 'fill8' })
  })

  // time(): slow one deliberate beat down to an exact three seconds, so the last
  // field types out in emphasized slow motion.
  await time(3000, async () => {
    await page
      .getByLabel('Billing email')
      .pressSequentially('[email protected]', {
        delay: 40,
        editId: 'pressSequentially2',
      })
  })

  await page.waitForTimeout(500)
  await resetZoom()
})

Looks filmed, not scripted

Natural motion blur on every zoom and move.

Show the generated e2e test file
Looks filmed, not scripted

Zooms and cursor moves get natural motion blur on render.

import { autoZoom, hide, resetZoom, video } from 'screenci'

// Own intent (strong zoom + cursor motion blur) plus the shared marketing
// render styling: light radial-gradient background and framed recording tile.
video.renderOptions({
  zoom: { motionBlur: 1 },
  mouse: { motionBlur: 1 },
  output: {
    background: {
      backgroundCss:
        'radial-gradient(circle at top left, #dbeafe 0%, #eff6ff 28%, #f8fafc 58%, #e0f2fe 100%)',
    },
  },
  recording: { size: 0.9, roundness: 0.04, dropShadow: 1 },
})('Motion blur', async ({ page }) => {
  await hide(async () => {
    await page.goto('https://demo.screenci.com/analytics')
    await page.waitForLoadState('networkidle')
  })

  // Every cursor move goes to something real: switch analytics views, glance at
  // the headline each view reveals, then pan back up to Overview. The tight zoom turns each hop into a fast pan, so both the camera
  // and the cursor show motion blur on moves that read as intentional.
  await autoZoom(
    async () => {
      const tab = (name: string) => page.getByRole('button', { name })
      const stat = (label: string) =>
        page.getByText(label, { exact: true }).first()

      await tab('Revenue').click()
      await page.waitForTimeout(400)
      await stat('ARR').hover()
      await page.waitForTimeout(600)

      await tab('Retention').click()
      await page.waitForTimeout(400)
      await stat('Net retention').hover()
      await page.waitForTimeout(600)

      await tab('Cohorts').click()
      await page.waitForTimeout(400)
      await page.getByRole('heading', { level: 3 }).first().hover()
      await page.waitForTimeout(600)

      await tab('Overview').click()
      await page.waitForTimeout(400)
    },
    { amount: 0.5, duration: 450, zoomOutDuration: 350, easing: 'ease-in-out' },
  )
  await page.waitForTimeout(500)
  await resetZoom({ zoomOutDuration: 350 })
})

Your design system, inside the video

Callouts and rings, anchored to your live UI.

Show the generated e2e test file
Your design system, inside the video

Rings, callouts, badges, and captions built with your own React, Vue, Svelte, or Solid components, or plain HTML and CSS. Anchored beside live elements, styled with your app's colors, and timed in your .screenci.ts file.

import { hide, video } from 'screenci'
import type { Locator } from '@playwright/test'

// Programmatic overlays: rings, callouts, and captions from code, anchored to
// the live UI and timed to the action. React and plain HTML side by side,
// over the demo app's deal pipeline.
video.overlays({
  // An accent ring sized to the live element.
  ring: (target: Locator) => ({ path: './overlays/ring.html', over: target, margin: 6 }),
  // Your React component, placed beside a live element.
  deal: (target: Locator) => ({
    path: './overlays/DealCallout.tsx',
    props: { company: 'Northwind Labs', value: '$61,200', probability: 0.75 },
    anchor: target, side: 'right',
  }),
  // A React lower-third and a plain-HTML chip at fixed spots.
  card: { path: './overlays/Callout.tsx', x: 72, y: 760, width: 640 },
  htmlChip: { path: './overlays/chip.html', x: 1440, y: 72, width: 400 },
})('Programmatic overlays', async ({ page, overlays }) => {
  await hide(() => page.goto('https://demo.screenci.com/pipeline'))

  const card = page
    .locator('[data-stage="Negotiation"] [data-deal-card]')
    .filter({ hasText: 'Northwind Labs' })
  const ring = overlays.ring(card)
  const deal = overlays.deal(card)
  await ring.start()
  await deal.start()
  await page.waitForTimeout(1800)
  await deal.end()
  await ring.end()

  await card.dragTo(page.locator('[data-stage="Closed won"]'))

  await overlays.card.start()
  await overlays.htmlChip.start()
  await page.waitForTimeout(2600)
  await overlays.htmlChip.end()
  await overlays.card.end()
})

Vertical for social, one setting

Change the aspect ratio, nothing else.

Show the generated e2e test file
Vertical for social, one setting

Switch aspectRatio to 9:16 and the recorder opens a portrait viewport automatically. No cropping, no reframing.

import { hide, video } from 'screenci'

// Vertical for social, one setting: the same demo app recorded at 9:16. The
// app's responsive layout takes over (the sidebar collapses into a top nav),
// and the render frames it for Reels, Shorts, and TikTok.
video
  .renderOptions({
    recording: { size: 0.96, roundness: 0.04 },
    output: {
      aspectRatio: '9:16',
      quality: '1080p',
      background: {
        backgroundCss:
          'radial-gradient(circle at top left, #dbeafe 0%, #eff6ff 28%, #f8fafc 58%, #e0f2fe 100%)',
      },
    },
  })
  .recordOptions({ aspectRatio: '9:16', quality: '720p' })(
  'Vertical video',
  async ({ page }) => {
    await hide(async () => {
      await page.goto('https://demo.screenci.com/')
      await page.waitForLoadState('networkidle')
    })

    await page.waitForTimeout(1200)
    await page.mouse.wheel(0, 700)
    await page.waitForTimeout(1500)
    await page.getByRole('button', { name: 'Inbox' }).click()
    await page.waitForTimeout(1500)
    await page.getByText('Onboarding timeline for the board').click()
    await page.waitForTimeout(1800)
  },
)

Tell the narrator how to sound

Direct tone and delivery per line.

Show the generated e2e test file
Tell the narrator how to sound

Direct tone and delivery per cue with a plain-English style prompt, from a calm guide to a full pirate. Expressive voice models unlock even more languages. Available on every plan.

import { video, voices } from 'screenci'

// Each cue carries its own style prompt, so the same voice adapts its
// delivery per line. Write the direction in plain English.
video.narration({
  hook: {
    cue: 'Every narration line can have its own speaking style.',
    voice: {
      name: voices.Nora,
      style: 'Warm and intriguing, drawing the viewer in.',
    },
  },
  pirate: {
    cue: 'Arr! Even a swashbuckling pirate can guide the tour, if that be the style ye prompt.',
    voice: {
      name: voices.Nora,
      style: 'A boisterous pirate captain, gravelly and full of swagger.',
    },
  },
  whisper: {
    cue: 'Or lean right in, because the best part is a little secret.',
    voice: {
      name: voices.Nora,
      style: 'A hushed, conspiratorial whisper, close to the mic.',
    },
  },
  outro: {
    cue: 'Write the style in plain English. The voice does the rest.',
    voice: {
      name: voices.Nora,
      style: 'Confident and conclusive, landing the final thought.',
    },
  },
})('Per-cue speaking style', async ({ page, narration }) => {
  await page.goto('https://demo.screenci.com/')

  await narration.hook()
  await narration.pirate()
  await narration.whisper()
  await narration.outro()
})

Included with every video

Everything a finished tutorial needs

Nothing to add in post. The voice, the camera, the subtitles, the languages, and the link come out of the same prompt.

Narrated, zoomed, and subtitled

A natural AI voice reads each step in sync, the camera follows the action, the cursor moves like a hand, and subtitles are on from the first take. Pick the voice and its tone per line, or bring your ElevenLabs API key and narrate in a clone of your own voice.

Narration guide

Re-records itself

The script is a test, so every take is identical. When the product changes, one click records it again.

How recording works

Screenshots too

Docs figures and README images are stills from the same script, branded, cropped, and re-captured when the UI changes.

Screenshots guide

Edited in the browser

Support and docs rewrite a line, swap the voice, or cut a slow section. No ticket to engineering; edits sync back to the script.

Make videos by prompt

One URL, forever

Embed a CDN link once in your help center or docs. It always serves the version you approved.

Public URLs and embeds

Overlays in your brand

Ask for an intro card or a callout and the agent draws it with your own components, so it matches the product.

Overlays guide

Recorded on your machine

Your source code and browser session stay put. Only the recording, its timing data, and the video scripts are uploaded, and redact() masks secrets before capture. The recorder is open source.

What gets uploaded

Optional, for engineering teams

A bonus: every tutorial is also an end-to-end test

Nothing here is required: you get the full product by pasting the prompt on a laptop and editing in the browser. But the script behind each video happens to be a normal Playwright test, so if your engineers want to, they can keep it in the repository and let CI re-record it on every release.

  • Lives in your repository

    The agent commits the video scripts next to your code. Reviewed, versioned, and readable by anyone who reads a Playwright test.

  • Re-recorded on every release

    Add to CI sets up a pipeline on your own runners, so the videos always show the current product.

  • A broken flow fails the build

    If the flow a tutorial walks through stops working, the run fails like any other test, before the stale video reaches a customer.

  • Costs nothing extra

    Recording and previews are free however often CI runs them. You pay only for exports.

FAQ

Common questions

Everything else is in the docs, and the prompt itself explains what it is about to do before it does it.

Read the docs
What kind of videos is this for?

Tutorials: help-center walkthroughs, onboarding, and docs screenshots, the videos that have to stay correct as your product changes. Launch and release clips work the same way. See make videos by prompt.

Who on the team pastes the prompt?

Whoever already has Claude Code or Codex open: a CTO, an engineer, a product owner. That one paste sets the project up. After that, support and docs edit narration and wording in the browser with no agent at all, and once the project records from CI, Re-record is one click in the app.

Do I need to know how to code?

No. You describe the video in one sentence and a coding agent writes and runs everything else. Under the hood the video is a script, but you never have to open it: narration, voices, overlays, and languages are all editable in the web editor. See Make videos by prompt.

Can the narration be my own voice?

Yes. Record a short sample in the browser, or drop in an audio or video file, and ScreenCI clones it through your own ElevenLabs API key (added once on the Branding page, encrypted, never shown again). The clone works on every plan; voices from your ElevenLabs library are available on higher plans. See ElevenLabs voices.

Which coding agents work?

Any agent or harness that can run a shell command: Claude Code, Codex, Cursor, pi.dev, desktop agents, and the like. The prompt tells the agent where to fetch its instructions, and the one-time code connects it to your project. The agent will only ever ask you to sign in to your own product in a browser it opens, never for a password.

Do I need to know how to use the terminal?

No. You paste the prompt into the agent and it runs every command itself: installing ScreenCI, writing the script, recording, and opening the result in your browser. If the agent needs anything from you, it asks in plain language. See Make videos by prompt.

Does my app or source code leave my machine?

Recording runs locally against your own app, and your app's source code never leaves your machine. What gets uploaded is the screen recording with its timing data, and the text files of the screenci/ folder: the config and the video scripts, so the web app can show them and hand them to a coding agent (set uploadSources: false to keep them off). Never env files, lockfiles, or anything outside that folder. Full list of what is sent. The CLI is open source, so you can verify that yourself.

What happens when my UI changes?

Click Re-record, or let CI do it on the next push, and the tutorial re-renders against the new UI. If the flow itself broke, the run fails like an end-to-end test, so you find out before your users do. See Repository and CI.

Can engineers keep the videos in the repository?

Optionally. Run the prompt inside the repository and the agent commits the scripts on a branch, and Add to CI sets up a pipeline that re-records them on every push. Each video is then a Playwright end-to-end test your CI already knows how to run. See Repository and CI.

Is it open source?

The CLI and the authoring API are MIT licensed on GitHub, and recording runs locally with them. Rendering (narration, camera, subtitles, overlays), the web editor, and hosting are a paid service and are not open source.

What happens to my videos if ScreenCI goes away?

The scripts are Playwright tests in your own repository, so they keep running as tests. Exports are MP4 files you download (npx screenci export writes them to ./exports), so you can host them anywhere. Only the hosted public URLs depend on the service.

What does it cost?

Trying it is free and needs no account: three trial recordings with live previews. On every plan, recording and live previews stay free; only rendered exports count, so costs follow the videos you publish, not the runs you make. See pricing for exports, languages, and team plans.

Why not just use Playwright's built-in video recording?

Playwright's recordings are raw debug captures. ScreenCI turns the same script into a finished product video: AI narration synced to each step, an auto-zoom camera, animated cursor and typing, overlays from your own React/Vue/Svelte components, subtitles, translations, and a permanent CDN URL. See what a script looks like.

Free to try, no account

Paste one prompt. Get your first tutorial.

Three trial recordings with live previews, straight from the prompt at the top of this page. Sign up only when you want to keep them.

Works with Claude Code, Codex, Cursor, pi.dev, and any agent that runs shell commands.

Rolling it out across a team?Book a 30-minute call