Skip to content

Annotated UI screenshots, described as data.

MIT · Node 20+ · driven by Playwright

Annotated screenshot automation for product documentation.

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.

Take your first screenshot
Or install:
npm i -D shotlist playwright
screenshots/recipes/recipe-anatomy.yaml
name: recipe-anatomy
install: public

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/demo/order-row.yaml
name: order-row
install: public

clip:
  css: 'li, div'
  contains: Acme Corp
  matching: '\$\d'
  maxChildren: 12
  pick: smallest
  pad: 20

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

callouts:
  - { mark: amount, text: What they owe, place: top }
  - { mark: status, text: Where it stands, place: bottom }

npm run demo:generate runs this file against the Ledger example and regenerates the image.

Ledger order row annotated at its amount and status
Ledger's Acme Corp order row. “What they owe” points to $42.00; “Where it stands” points to Open.

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 1.48 or later 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:4321
  serve: npm run dev
  viewport: { width: 1280, height: 800 }
  scale: 2
  allow:
    - ws://localhost:4321
    - fonts.googleapis.com
    - fonts.gstatic.com
    - cdn.usefathom.com

paths:
  recipes: screenshots/recipes
  out: screenshots/out

install:
  public: public
  social: public/social