Skip to content

Queries

A query says which element to act on or measure. Keys combine: sources choose the candidates, filters narrow them, traversal moves from them, and selection picks one.

Key a query on what a person can see. text, heading, role+name and label survive a restyle. A generated class name does not survive the next build.

Sources

Key Matches
cssA CSS selector
role+nameARIA role and accessible name
labelForm control by its label
placeholderInput by placeholder text
testiddata-testid
textElement whose trimmed text equals the value
startsWithElement whose trimmed text begins with it
headingh1h6 with this exact text

text and startsWith also narrow a source written alongside them. exact: true makes role+name, label and placeholder match the whole string, case-sensitively.

role, label, placeholder and testid cannot be used inside span: or within:. The browser resolves them before the page is searched, so there is nothing for a nested one to narrow. Use css, text, startsWith or contains there.

Filters

Key Keeps elements that…
containscontain this text
containingAllcontain all of these strings
matchinghave text matching this regular expression
maxChildren / minChildrenhave at most / at least this many children
minWidth maxWidthare within these widths
minHeight maxHeightare within these heights
narrowerThan / widerThanare narrower / wider than this
visiblehave a non-zero size
withinsit inside an already-resolved mark

Sizes take pixels (400) or viewport units (95vw, 50vh). matching is a regular expression run inside the page, so one that backtracks is bounded by site.timeout rather than hanging the run.

within takes a name, or a query written out. Two kinds of name resolve: clip, which is the region being captured, and any mark declared above this one — marks resolve in the order they are written, so an earlier one can narrow a later one. A name that has not resolved yet is an error rather than a miss.

It belongs to marks, mask and check.ignore, which run after the clip is known. A step cannot use it: setup runs before anything has been measured.

Traversal

Key Moves to
ancestorThe first ancestor matching the filters given
parentThe parent element
child: nThe nth child; -1 is the last one
childrenAll children

ancestor climbs from the element's parent — an element is never its own ancestor, so a box that already fits the filters is not the answer. It takes pick: nearest (default) or pick: outermost; outermost keeps climbing while the parent also matches, which is how you reach a modal's card rather than stopping at the heading inside it.

Selection and shape

Key Effect
pick: first | last | smallest | largestWhich candidate to use
nth: nThe candidate at this position; -1 is the last
pad: nGrow the resulting box on all sides
grow: { top, right, bottom, left }Grow it on specific sides
span: [query, query]The bounding box of several queries
rect: [x, y, width, height]A literal box, for recipes with no page

nth and child count from the end when the number is negative, the way Array.at does: -1 is the last, -2 the one before it. pad and grow add room around a box and cannot be negative.

Two patterns worth knowing

pick: smallest is what makes "the row containing X" work. Every ancestor of the row also contains the text, right up to the body — without it you clip the whole page.

screenshots/recipes/order-row.yaml
# The smallest box holding both a name and an amount.
clip:
  css: 'li, div'
  contains: Acme Corp
  matching: '\$\d'
  maxChildren: 12
  pick: smallest

ancestor with pick: outermost reaches a card inside an overlay. The width filter is doing real work: it is what separates the dialog's card from the full-screen backdrop behind it, which also contains the heading.

screenshots/recipes/edit-order.yaml
# The card owning a heading, climbed out of its full-screen overlay.
marks:
  dialog:
    heading: Edit order
    ancestor: { narrowerThan: 95vw, pick: outermost }

Finders

To reuse a query across recipes, define it under finders in the config and call it by name. $1 and $2 stand for the arguments it is called with. It is worth doing the moment the same shape appears in a third recipe — a project's recipes should read like the product, not like CSS.

shotlist.config.yaml
finders:
  listRow:
    css: 'li, div'
    contains: $1
    matching: '\$\d'
    maxChildren: 12
    pick: smallest

  panel:
    heading: $1
    ancestor: { narrowerThan: 95vw, pick: outermost }
screenshots/recipes/order-row.yaml
clip: { listRow: Acme Corp }
marks:
  dialog: { panel: Edit order }