Skip to content
runsascodedPublic

About

Automated screenshots with Playwright/Puppeteer — wait for selectors, configurable viewports, CLI

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

scrns

npm version

Automated screenshots and screencasts (GIF/video) with Playwright or Puppeteer — wait for selectors, configurable viewports, scroll positioning, action timelines, and downloads.

Also available on GitLab. Reusable CI integrations: GitHub Action (v1 branch), GitLab CI component.

Install

pnpm add scrns
# or
npm install scrns

Playwright is included by default; install its Chromium binary once per machine:

pnpm exec playwright install chromium

To use Puppeteer instead (or alongside; it downloads Chrome automatically on install):

pnpm add puppeteer

CLI Usage

scrns [options]

Options

Flag Description
-b, --browser-arg <arg> Additional browser launch arg (repeatable)
-c, --config <path> Config file path (default: scrns.config.{ts,js,json})
-C, --color-scheme <scheme> Emulated prefers-color-scheme: light or dark (overrides top-level colorScheme; per-shot values still win)
-d, --download-sleep <ms> Sleep while waiting for downloads (default: 1000)
-D, --docker Run in Docker for reproducible output (see Docker mode)
--docker-image <image> Docker image (default: auto-detect from Playwright version)
--docker-platform <platform> Docker platform (default: linux/amd64)
--docker-scrns <ver> scrns version for Docker (npm version, or source SHA / gh:<sha> / gl:<sha>, resolved to its dist build)
-E, --engine <name> Browser engine: playwright or puppeteer (default: auto-detect, prefers Playwright)
-h, --host <host> Hostname or port (numeric port maps to 127.0.0.1:port)
-H, --headful Run browser in headful mode (override config headless)
-i, --include <regex> Only generate screenshots matching this regex
-l, --load-timeout <ms> Timeout waiting for selector (default: 30000)
-o, --output <dir> Output directory (default: ./screenshots)
-s, --selector <css> Default CSS selector to wait for
-T, --screenshot-timeout <ms> Timeout per screenshot capture (default: 30000)
--https Use HTTPS instead of HTTP

Example

# Auto-detect config, use localhost:3000
scrns

# Specify port and config
scrns -h 8080 -c my-config.ts

# Filter to specific screenshots
scrns -i "home|about"

# Use Puppeteer instead of Playwright
scrns -E puppeteer

# Same config, dark theme, separate output dir
scrns -C dark -o screenshots/dark

Preview Mode

scrns preview (aliased record) opens a headful browser for interactively composing screenshots. Adjust the view and resize the window, then capture with Enter (terminal) or Ctrl+Shift+S (browser):

# Preview a specific config entry
scrns preview <name>

# Preview an arbitrary URL (no config needed)
scrns preview --url 'http://localhost:3000/?view=map'

On capture, prints the resulting state as a config snippet:

Captured "og":
  query: '?view=map&zoom=12'
  width: 1200
  height: 800

Docker Mode

Screenshots rendered on macOS differ from Linux CI output (fonts, antialiasing). scrns -D/--docker runs the capture inside a mcr.microsoft.com/playwright container matching the GHA runner environment, so locally-generated screenshots are byte-identical to CI:

scrns -D
  • The image defaults to mcr.microsoft.com/playwright:v<version>-noble, matching the installed Playwright version; override with --docker-image.
  • When run from a scrns checkout (local dev), the local build is packed and mounted; otherwise the installed version is reused. Pin explicitly with --docker-scrns <ver>: an npm version, or a source SHA (bare = GitHub, or gh:<sha> / gl:<sha>), which is resolved to the dist-branch commit CI built from it (source commits aren't installable: dist/ is untracked).
  • docker, dockerImage, and dockerPlatform can also be set in the config file.
  • Env vars referenced by headers ($NAME) are passed into the container by name (docker run -e NAME), so their values stay off the command line. storageState files aren't mounted.

Config File

Create scrns.config.ts (or .js/.json). The config can be a flat Screens map:

export default {
  'home': {
    query: '',
    width: 1200,
    height: 800,
  },
  'about': {
    query: 'about',
    selector: '.main-content',
  },
  'footer': {
    query: '',
    scrollTo: 'footer',
    scrollOffset: 20,
  },
}

Or a Config with top-level options and a screenshots key, so that host, output, etc. live in the config file instead of CLI flags:

import { Config } from 'scrns'

const config: Config = {
  engine: 'playwright',  // or 'puppeteer' (default: auto-detect, prefers Playwright)
  host: 3456,
  output: 'public/img/screenshots',
  selector: '.app',
  screenshots: {
    'home': { query: '' },
    'about': { query: 'about' },
  },
}

export default config

Top-level config options (engine, host, https, output, selector, loadTimeout, downloadSleep, browserArgs, headless, screenshotTimeout, colorScheme, docker, dockerImage, dockerPlatform) are overridden by their corresponding CLI flags when both are specified. headers, scale, and storageState can also be set top-level (config-only; no CLI flags), as defaults for every screenshot.

Config Options

Option Type Default Description
path string {name}.png Output path (relative to outputDir, or absolute)
query string '' URL path/query (appended to baseUrl)
width number 800 Viewport width
height number 560 Viewport height
selector string - CSS selector to wait for before capturing
loadTimeout number 30000 Timeout in ms for selector wait
preScreenshotSleep number 0 Sleep in ms before taking screenshot
scrollY number 0 Scroll Y pixels before screenshot
scrollTo string - CSS selector to scroll into view
scrollOffset number 0 Offset pixels above scrollTo element
download boolean false Set download behavior instead of screenshot
downloadSleep number 1000 Sleep in ms while waiting for download
browserArgs string[] - Additional browser launch args for this screenshot
headless boolean true Override headless mode for this screenshot
screenshotTimeout number 30000 Timeout in ms per screenshot capture
headers Record<string, string> - Extra HTTP headers for every request (merged over top-level headers); $NAME / ${NAME} expand from env
scale number 1 Device scale factor (e.g. 2 for retina captures)
storageState string - Playwright storage-state JSON (cookies + localStorage); Playwright only
colorScheme 'light' | 'dark' 'light' Emulated prefers-color-scheme (overrides top-level colorScheme / -C)
crop string | { selector, padding? } - Capture only the first element matching selector (plus padding CSS px, clamped to the page)

Auth headers, crop, scale

const config: Config = {
  host: 'app.example.com',
  https: true,
  // Sent with every request from the page (navigations and in-app `fetch`es)
  headers: { Authorization: 'Bearer $SITE_TOKEN' },
  scale: 2,
  screenshots: {
    'health-panel': { query: 'health', crop: { selector: 'section.store', padding: 16 } },
    'treemap': { query: '', crop: '.treemap' },
  },
}
  • headers: $NAME / ${NAME} refs are expanded from the environment at run time; an unset var is an error (raised before any browser launches). TS/JS configs can also just read process.env. Header values are never logged (only names). Headers go to every request the page makes, including third-party origins (CDNs, fonts), so only use them on pages you trust with the token.
  • crop: the element box is measured once, after load / scroll / preScreenshotSleep, in document coordinates (so off-screen elements work); for screencasts the same region is used for every frame. A selector that matches nothing is an error.
  • scale: output dimensions are multiplied by it (a crop or viewport of W×H at scale: 2 yields 2W×2H pixels).

Color scheme

colorScheme: 'dark' (top-level, per-shot, or -C dark) emulates prefers-color-scheme: dark, so @media (prefers-color-scheme: dark) rules, matchMedia and <picture><source media> all see dark mode from the first paint. The default is light on both engines, regardless of the host OS theme. Apps whose dark mode is a URL param or localStorage key don't need this; set that via query or a js action instead.

Screencast Options

Adding an actions array to a config entry turns it into a screencast. The output defaults to .gif.

Option Type Default Description
actions ScreencastAction[] - Action timeline (presence triggers screencast mode)
fps number 15 Frames per second for GIF capture
gifQuality number 10 GIF quality: 1-30, lower = better
loop boolean true Whether the GIF should loop
videoCrf number 23 CRF quality for video output (lower = better, requires ffmpeg)

Screencast Actions

Action Fields Description
wait duration Pause for duration ms
key key, duration Hold key(s) for duration ms (e.g. 'Shift+ArrowLeft')
keydown key Press and hold key(s)
keyup key Release key(s)
type text Type text
click x, y, button? Click at coordinates
hover x, y or selector, index? Move mouse to coordinates, or to the center of the indexth element matching selector
drag from, to, duration, button? Drag between coordinates over duration ms
animate frames, eval, frameDelay? Deterministic frame-by-frame capture (see below)

The animate action calls eval as (frameIndex, totalFrames) => ... for each frame, capturing a screenshot after each call. This produces deterministic, idempotent GIF output.

Screencast Example

export default {
  // Screenshot (unchanged)
  'og': {
    query: '?view=default',
    width: 1200,
    height: 800,
    selector: '#root',
    path: 'og.png',
  },
  // Screencast: hold a key to rotate a 3D view
  'hero': {
    query: '?view=3d',
    width: 800,
    height: 500,
    selector: '#root',
    preScreenshotSleep: 3000,
    path: 'hero.gif',
    fps: 15,
    actions: [
      { type: 'wait', duration: 1000 },
      { type: 'key', key: 'Shift+ArrowLeft', duration: 4000 },
      { type: 'wait', duration: 1000 },
    ],
  },
  // Screencast: deterministic frame-by-frame animation
  'rotate': {
    query: '?view=3d',
    width: 800,
    height: 500,
    selector: '#root',
    preScreenshotSleep: 3000,
    path: 'rotate.gif',
    fps: 15,
    actions: [
      { type: 'wait', duration: 500 },
      { type: 'animate', frames: 60, eval: '(i, n) => { window.setBearing(i * 360 / n) }' },
      { type: 'wait', duration: 500 },
    ],
  },
}

Video Output

Set the output path to .mp4, .mkv, .mov, or .webm to produce video output via ffmpeg instead of GIF. This gives frame-accurate recordings with proper codec compression (H.264 or VP9).

Requires ffmpeg on $PATH.

export default {
  'demo': {
    query: '?view=demo',
    width: 800,
    height: 600,
    selector: '#root',
    path: 'demo.mp4',
    videoCrf: 18,  // lower = better quality
    actions: [
      { type: 'wait', duration: 1000 },
      { type: 'key', key: 'ArrowRight', duration: 3000 },
      { type: 'wait', duration: 1000 },
    ],
  },
}

Codec selection is based on extension: .webm → VP9 (libvpx-vp9), all others → H.264 (libx264).

To derive a GIF from video output:

ffmpeg -i demo.mp4 -vf "fps=15,scale=400:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" demo.gif

Programmatic Usage

import { takeScreenshots, resolveEngine, ScreencastConfig } from 'scrns'

// Optional: explicitly choose an engine (auto-detects if omitted)
const engine = await resolveEngine('playwright')

await takeScreenshots({
  'home': { query: '' },
  'about': { query: 'about', selector: '.content' },
  'demo': {
    query: 'demo',
    actions: [
      { type: 'wait', duration: 1000 },
      { type: 'key', key: 'Enter', duration: 100 },
      { type: 'wait', duration: 2000 },
    ],
    fps: 10,
  } satisfies ScreencastConfig,
}, {
  baseUrl: 'http://localhost:3000',
  outputDir: './screenshots',
  defaultSelector: '#app',
  include: /home|demo/,
  log: console.log,
  engine,  // omit to auto-detect
})

Used by

  • jc-taxes (usage) — Jersey City property tax map (deck.gl 3D visualization)
  • use-kbd (usage) — Omnibars, editable hotkeys, search, and keyboard-navigation for React apps
  • ctbk (usage) — Citi Bike trip data explorer
  • apvd (usage) — Area-proportional Venn diagrams

About

Automated screenshots with Playwright/Puppeteer — wait for selectors, configurable viewports, CLI

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages