Take annotated UI screenshots from YAML recipes, using Playwright.
shotlist opens your running site, drives it to the state you describe, clips a region, draws callouts, and writes the image where you want it. You describe each screenshot in a YAML recipe.
shotlist.dev/docs is the reference. It covers each key, step verb, and query primitive. This README covers setup and common commands.
shotlist requires Node.js 20 or newer and a project
with a package.json. Create one with
npm init if needed.
Install shotlist and Playwright from the npm registry:
npm install -D shotlist playwrightUse the equivalent pnpm or Yarn command if your project uses one of them.
shotlist keeps Playwright optional because Playwright downloads browsers during package
installation. Install it when shotlist writes an image, including runs with --check or a
source: file recipe. shotlist draws file-recipe callouts in a page. The --init,
--help, and recipe-listing commands do not launch a browser.
npx shotlist --initshotlist writes a commented shotlist.config.yaml and a starter recipe. You can create
them by hand instead.
1. Configure the project once in shotlist.config.yaml at the project root:
site:
url: http://localhost:3000
viewport: { width: 1440, height: 900 }
scale: 2
theme: dark
install:
guide: content/guide/images2. Write a recipe in screenshots/recipes/order-row.yaml:
name: order-row
install: guide
setup:
- click: { role: button, name: Orders }
clip:
css: '.order-row'
contains: Acme Corp
pad: 20
marks:
amount: { within: clip, text: $42.00 }
status: { within: clip, text: Open }
callouts:
- { mark: amount, text: What they owe }
- { mark: status, text: Where it stands }3. Shoot it:
npx shotlist order-row --installshotlist writes the image to screenshots/out/order-row.png. With --install, it copies
the image to content/guide/images/order-row.png. PNG is the default. Set image.format
to jpeg or webp in the project config or recipe.
| Page | What it covers |
|---|---|
| Your first screenshot | A lesson: install, a recipe, a callout |
| Add shotlist to a project | Setting up in an app you already have |
| Configuration file | Every key, starting the site, style and fonts |
| Recipe file | Every field, and annotating an existing image |
| Steps | The step vocabulary |
| Queries | Sources, filters, traversal, frames, finders |
| Callouts and masks | Labels, numbered discs, masking |
| Macros and data files | Sharing setup, driving a shot from a list |
| Command line | Every flag, the API, editor and agent support |
| Keeping a screenshot current | --check, diffs, and a shot that changes |
| Undo what a shot changed | teardown, for a recipe that writes |
| What a configuration can do | What a run is allowed to reach |
npx shotlist --init # write a starter config and recipe
npx shotlist # list every recipe
npx shotlist <name> [<name>…] # shoot into paths.out
npx shotlist <name> --install # …and copy to its install destination
npx shotlist --all --install # shoot everything
npx shotlist --all --keep-going # …carrying on past a recipe that fails
npx shotlist --check # compare against committed images
npx shotlist --check --diff # …and write a before/after/changed image
npx shotlist --check --json # …and report it as JSON on stdout
npx shotlist --lint # check every YAML; no browser, no site needed
npx shotlist --work-limit executedSteps=20000 <name>
# let this Run execute more Steps
npx shotlist --login admin # sign in by hand, and save the session
npx shotlist --help # the full list, from the toolYour caller grants Operator authority to openRun, then makes every Capture and Checking
request through that Run:
import { openRun, type CaptureReport, type CheckReport, type RunProgress } from 'shotlist'
const run = openRun({ untrusted: false }, 'shotlist.config.yaml')
const one: CaptureReport = await run.capture({ recipes: ['order-row'] })
const group: CaptureReport = await run.capture({
recipes: ['order-row', 'account-menu'],
install: true,
})
const all: CaptureReport = await run.capture({ all: true })
const controller = new AbortController()
process.once('SIGINT', () => controller.abort('SIGINT'))
const checked: CheckReport = await run.check({
all: true,
diff: true,
signal: controller.signal,
onProgress: async (progress: RunProgress) => console.log(progress.type),
})
for (const result of checked.results) {
if (result.status === 'changed') console.log(result.name, result.diff)
if (result.status === 'failed') console.error(result.name, result.error)
}
for (const failure of checked.failures) console.error(failure.resource, failure.error)
for (const warning of checked.warnings ?? []) console.warn(warning)A one-Recipe request uses the same interface and report as a group. Named Recipes keep
caller order; { all: true } selects every Recipe in recipe-name order. Capture results
are captured, failed, cancelled, or not-attempted; group.installation accounts for
Committed images only after every selected Capture and required cleanup finishes. Checking
retains same, changed, new, and skipped findings beside operational failures,
cancellation, and unattempted Recipes. Its report also includes environment drift,
Ignore-region counts, and optional diff-image paths.
Both reports are immutable. Request-level site, browser, and Baseline failures live in
failures; progress-observer failures live in warnings; cancellation details live in
cancellation. keepGoing: true attempts later Recipes after a Recipe failure. An awaited
onProgress observer receives request, Recipe, retry, and installation facts in order. The
Run remains reusable after a request settles and rejects overlapping requests.
shotlist rejects invalid requests with ShotlistError. It also rejects a hand-built Run
before it touches the browser, filesystem, network, or a process. The caller grants
Operator authority, including any numerical Work limit changes; the Project config does
not.
Replace shoot(run, recipe, options) with a name-based request such as
run.capture({ recipes: [recipe.name] }). Replace check(run, recipes, options) with
run.check({ recipes: names }), where names is a string array. Handle the returned report
rather than a direct ShotResult or CheckResult[], and remove caller-owned browser setup.
Use install: true on the Capture request; the Run defers Installation until Capture and
cleanup finish.
This package-interface change does not change Recipe or config files, and it does not change CLI commands or flags.
Pass authority to lint because a malformed Project cannot open a complete Run:
lint({ untrusted: false }, 'shotlist.config.yaml'). Use parseConfig, parseRecipe,
parseMacro, parseLibrary, and parseQuery to parse in-memory values without Operator
authority.
Every Run applies Work limits to Library document size, authored structure, Macro
expansion, actual Steps, and elapsed Recipe work. A Project cannot raise them. An Operator
may change a numerical limit for one command with --work-limit name=value, through
protected SHOTLIST_WORK_LIMITS, or through OperatorAuthority.workLimits in TypeScript.
shotlist --lint rejects predictable excess before shotlist starts a site or browser.
Describe screenshots with recipe vocabulary rather than JavaScript. Contributors add a
step verb or query primitive for screenshots the vocabulary cannot express. shotlist does
not support eval: or other executable recipe fields. See
CONTRIBUTING.md.
Automation may run a config from a fork or another contributor. In every mode, shotlist
blocks secret-looking paths such as .env, .git, and .ssh. It checks browser requests, redirects, WebSockets, and readiness probes against approved protocol, host, and port values. In --untrusted mode, shotlist starts no configured process, loads no stored Session, accepts no Project-provided Network destination approvals, and confines paths to approved roots. Use --allow or protected SHOTLIST_ALLOW settings to grant exact destinations, and keep hostile Projects inside an isolated runner.
Read the full policy and its limits in the security model.
You can contribute code or documentation, from bug fixes and tests to new features and typo corrections.
CONTRIBUTING.md covers setup, commands, code style, and the completion criteria.
MIT © Nicola Mustone. See LICENSE.