Skip to content

MIT · Node 20+ · driven by Playwright

Annotated UI screenshots, described as data.

shotlist opens your running site, drives it to the state you describe, clips a region, draws the callouts on it, and writes the image where you want it. Each screenshot is a YAML file. There is no per-screenshot code.

Read the docs npm i -D shotlist playwright
screenshots/recipes/recipe-anatomy.yaml
# The shot at the top of this page, described in full.
name: recipe-anatomy
install: site

setup:
  - click: { role: link, name: Docs }
  - wait: { css: '[data-recipe]' }

clip:
  css: '[data-recipe]'
  pad: 24

marks:
  drive:  { within: clip, text: 'setup:' }
  region: { within: clip, text: 'clip:' }
  draw:   { within: clip, text: 'callouts:' }

callouts:
  - { mark: drive,  text: Drive the page, place: left }
  - { mark: region, text: Clip a region,  place: left }
  - { mark: draw,   text: Draw on top,    place: right }
The recipe for a shot of this site's own docs page. Nothing here is a simplification — it is the whole file.

What a recipe contains

Three sections, read top to bottom: drive the page to a state, clip a region of it, draw on what the clip contains.

  1. setup:

    Drive the page to a state

    Twenty verbs — click, fill, hover, select, wait, and the rest — run against your site until the thing you want to photograph is on screen. Steps shared between shots go in a macro.

  2. clip:

    Clip a region of it

    The whole viewport, the whole page, or whatever an element query finds: the smallest box holding a name and an amount, a modal card climbed out of its overlay.

  3. callouts:

    Draw on top

    Say which side of a mark a label belongs on. shotlist works out the position, grows the canvas when a label needs room outside the shot, and moves one that would collide with another.

A recipe is data

A recipe is a YAML file, and nothing in it runs. There is no JavaScript step, and no plugin hook that turns into one.

Twenty verbs drive the page. A query language says what to point at — a row in a table, a modal, the amount in a cell.

A recipe can be read without knowing the codebase, and fixed by whoever notices it broke.

screenshots/recipes/order-row.yaml
name: order-row
install: guide

clip: { css: '.order-row', contains: Acme Corp, pad: 20 }

marks:
  amount: { within: clip, text: $42.00 }

callouts:
  - { mark: amount, text: What they owe, place: left }

Find an element by CSS, role, label or text, then narrow it down until one thing matches.

Checking screenshots against the site

shotlist --check re-shoots every recipe, compares each against the committed image, and exits non-zero if any of them moved. In CI that makes a screenshot which no longer matches the site a failing build.

Nothing else reports it. When an interface changes, the committed image goes on rendering exactly as before; it is only no longer true.

The parts a recipe does not decide — a clock, a live figure — are named rather than tolerated, so the check is worth reading. One running on a different machine from the one that took the images says so before it says anything else.

$ npx shotlist --check

same     order-row

same     modal-detail

CHANGED  billing-plan — 3.41% of pixels differ

          committed: content/guide/images/billing-plan.png

1 of 3 need attention

Getting started

Playwright is an optional peer dependency — shotlist does not install it, because its postinstall downloads browsers and a project consuming shotlist should not pay for that on every install.

npx shotlist --init writes the config and a first recipe. Edit them, add one file per screenshot, and shoot the set with npx shotlist --all --install.

Read the docs
shotlist.config.yaml
site:
  url: http://localhost:3000
  viewport: { width: 1440, height: 900 }
  scale: 2

install:
  guide: content/guide/images