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 screenshotnpm i -D shotlist playwright 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 } 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.
-
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.
-
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.
-
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.
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.
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
The rest of what it does
None of it is needed for a first screenshot.
-
site.serveStart the site it shoots
Names the command, waits until it answers, stops it afterwards — and uses a server you already have running rather than fighting it for the port.
-
maskPaint over a region
A clock, a live total, a face. The rest of the shot stays under drift detection instead of the whole image opting out.
-
check.ignoreCheck a shot whose subject changes
Leaves a region out of the comparison but shoots it as it is — and still reports the box moving, resizing or rendering nothing.
-
--diffWrite a diff image
A committed, re-shot and changed-pixels three-up per drifted shot. --json reports the run for something other than a person.
-
use:Share setup, and drive a shot from a list
A macro for the steps every recipe repeats, a data file for the rows to walk. One recipe, one screenshot per order.
-
source: fileAnnotate an image you already have
For a screen no script can reach — behind a sign-in you cannot automate, or an extension. Same callouts, same install.
-
--untrustedRun a config you did not write
A shot list only opens its own site and never reads a .env, in every mode. In CI, one flag takes away the rest.
-
SKILL.mdEditor and agent support
JSON Schemas generated from the same definitions that validate a run, and a skill that teaches an agent to write a query worth keeping.
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.
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