Your first screenshot
In this tutorial you will install shotlist into a small application and produce an annotated screenshot of part of it. You will finish with a PNG showing one row of an orders list, with two labelled arrows pointing at the parts that matter.
Everything runs on your own machine. You do not need an application of your own, and you will not write any JavaScript — everything you write here is YAML. The tutorial takes about fifteen minutes.
Before you begin
You need Node.js 20 or later, npm, and a terminal.
Step 1: get the example application
Download shotlist-example.zip and unzip it wherever you keep projects. Inside is a small fake invoicing screen called Ledger, with a documentation page alongside it. It has no dependencies of its own.
Start it:
cd shotlist-example
npm run dev Open http://localhost:3000. You should see a list of orders, one row per invoice, each with a customer name, a reference, an amount and a status. This is the application you are going to photograph.
If it reports that port 3000 is already in use, stop whatever is holding it, or run PORT=3001 npm run dev instead and use that port everywhere this tutorial says 3000 — including in site.url in step 3.
Leave it running, and open a second terminal in the same folder for everything that follows.
Step 2: install shotlist
npm i -D shotlist playwright pnpm add -D shotlist playwright yarn add -D shotlist playwright This takes a minute or two, because Playwright downloads a copy of Chromium. shotlist uses that browser to open the page and take the picture. For why the browser is a separate install, see Why Playwright is a peer dependency.
Step 3: point shotlist at the application
Create a file called shotlist.config.yaml in the root of the example project, beside package.json:
site:
url: http://localhost:3000
serve: npm run dev -
site.urlis where the application is running. It is the only key shotlist requires, and it also decides which addresses your recipes are allowed to open. -
site.serveis the command that starts the application. shotlist requestssite.urlfirst and uses the server you already started, so this line is what matters on a machine where nothing is running yet.
Step 4: write a recipe and take a picture
A recipe is one YAML file describing one screenshot. Create the folder screenshots/recipes/ and, inside it, a file called order-row.yaml:
name: order-row
clip: viewport Take it:
npx shotlist order-row You should see:
✓ order-row → screenshots/out/order-row.png Open screenshots/out/order-row.png. It is the whole browser window, which is rarely what documentation wants. Next you will narrow it to a single row.
Step 5: capture one region
Replace the recipe with this, and run the same command again:
name: order-row
clip:
css: 'li, div'
contains: Acme Corp
matching: '\$\d'
maxChildren: 12
pick: smallest
pad: 20 The image is now a single row: Acme Corp, INV-1042, $42.00, Open. The query narrowed down to it in stages:
csscollects every list item and every div on the page.containskeeps the ones holding the text "Acme Corp".matchingkeeps the ones whose text also holds a currency figure.maxChildrendiscards containers with a great many children.-
pick: smallestchooses between what is left. Both the row and the panel around it match everything above, and the row is the smaller of the two. padadds 20 pixels of room on every side.
Notice what the query does not mention: no class names, no positions in the document. Rename order-row in the stylesheet and this recipe still works.
Step 6: point at something
Now add two callouts: a labelled arrow to the amount, and another to the status.
name: order-row
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 } Two new sections appeared:
-
marksgives a name to each region you care about.within: cliplimits the search to the region being captured, which matters here: more than one row on the page says "Open", and only one of them is in your screenshot. -
calloutsis what gets drawn. Each entry names a mark, the label text, and which side of the region the label sits on.
Run the command again and open the image. Both regions are outlined in red, with an arrow running from a red label to each outline. shotlist made the canvas taller than the captured row so the labels have somewhere to sit. Your result should match this image:
Step 7: see what happens when a mark is wrong
This step breaks the recipe on purpose, so you recognize the error when you meet it for real.
Change text: Open to text: Opened and run the command again:
recipe "order-row": marks.status — no element matched {"text":"Opened","within":"clip"} No image was written. A recipe is an assertion that the page contains the things it points at, and shotlist would rather fail than publish a screenshot with an arrow pointing at nothing.
Change Opened back to Open and confirm the run succeeds again.
Step 8: number the parts instead of labelling them
When the prose beside a screenshot is already a numbered list, discs are tidier than labels. Replace the whole callouts section with a single numbered line:
name: order-row
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 }
numbered: [amount, status] Run the command once more. The labels and arrows are gone, and each region now carries a small red disc, numbered in the order you listed the marks.
Put the callouts section back before you move on — the next tutorial uses it.
Step 9: list what you have
npx shotlist With no arguments, shotlist prints every recipe it found. In a real project this folder holds one file per screenshot in your documentation, and npx shotlist --all re-takes all of them.
What you have learned
- installed shotlist and Playwright into a project
- written a configuration naming the application to photograph
- captured a whole window, then narrowed the capture to one region
- described that region by what a person sees rather than by its markup
- annotated it with labelled callouts, and with numbered discs
- seen shotlist refuse to produce a screenshot whose annotations no longer match the page
Next steps
- Keeping a screenshot current continues in the same project. It publishes this screenshot into Ledger's own documentation page, then detects when the application changes underneath it.
- Capture a page after interaction shows how to click and type before capturing. The example application has a "Load more" button to practise on.
- Recipe file lists every field a recipe can contain.