For the complete documentation index, see llms.txt. This page is also available as Markdown.

Selectors

Many Gaffa actions take a selector — a short piece of text that tells Gaffa which element on the page to work with, like a button to click or a table to read.

Most people write CSS selectors, and all of those work. But Gaffa uses Playwright's selector engine, so you can also match elements by their text or their visibility. XPath works too.

Writing selectors in JSON

Your selector goes inside a JSON string, so any double quotes inside it will break the request. Use single quotes instead.

This is valid:

"selector": "input[name='email']"

This is not:

"selector": "input[name="email"]"

You can also escape the double quotes with backslashes, but single quotes are easier to get right. This matters more than it looks, because most of the extensions below take a quoted argument.


Standard CSS

Every standard CSS selector works, including newer ones like :has(), :is() and :not(). Attribute selectors, combinators and structural selectors all behave exactly as they do in a browser.

Pseudo-elements such as ::before, ::after and ::placeholder can't be targeted, because there's no real element to return.

See the MDN CSS selectors reference for the full list.


Playwright extensions

Alongside standard CSS selectors, Gaffa supports Playwright's selector extensions.

Finding elements by text

Selector
What it matches

:has-text('foo')

Any element whose contents include that text anywhere inside it. Case-insensitive.

:text('foo')

The smallest element directly containing that text. Case-insensitive, trimmed.

:text-is('foo')

Exact match only. Case-sensitive, trimmed.

:text-matches('regex', 'flags')

Matches the element's text against a regular expression.

Use these when a button or link has no useful class or ID, which is common on sites built with utility CSS frameworks.

Other extensions

Selector
What it does

:visible

Matches only elements that are actually rendered and visible.

:nth-match(selector, n)

The nth match across the whole page, unlike :nth-child which counts within one parent.

:light()

Matches only the normal page DOM, ignoring content inside web components.

>>

Chains selector types together, for example css=... >> xpath=....

XPath

You can use XPath anywhere a selector is accepted. Start the selector with // or xpath=.

XPath is useful when you need to select an element by its relationship to another one, a parent, or a preceding sibling, which CSS can't express.


Elements inside iframes

An iframe is a separate web page embedded inside the one you requested. Embedded videos, payment forms, maps and comment widgets are usually iframes.

  • Same-origin iframes, meaning frames served from the same domain as the page, can be reached. Write your selector as normal, and Gaffa will find the element inside the frame; you don't need to target the frame first.

  • Cross-origin iframes, meaning frames served from a different domain, cannot currently be reached. Selectors won't match elements inside them. Embedded YouTube players, payment widgets and third-party tools are usually cross-origin.

To tell which you're dealing with, compare the iframe's src with the page's own URL. A different domain means cross-origin.

Elements inside web components

Some sites build parts of the page as web components, which keep their internal markup in a shadow DOM. Gaffa looks inside open shadow roots automatically, so you can write a selector for an element inside a component without doing anything special.

An element missing from a DOM capture may still be reachable.

capture_dom and generate_simplified_dom return the main page only. Iframes and web components both appear as empty tags, with no content. So if you can see something on screen but can't find it in a capture, don't assume it can't be selected. If the frame is same-origin, or the content is inside a web component, try the selector.


Examples

Click a button that has no ID or class

Fill a field identified by its label

Click the retry button in the row whose status says "Failed"

Wait for a success message inside a dialog


Traps worth knowing

  • :has-text() matches parents too. div:has-text('Submit') matches every div containing that text, including the outermost one. Use :text() when you want the tightest element, or scope it to a container first.

  • Generated class names change. Class names produced by build tools are rebuilt on every deploy. Prefer id, data-testid, aria-label or a text selector.

  • Avoid positional selectors. Playwright has deprecated :right-of() and the others, and matching on layout is unreliable — a small change moves things around.

  • Long selectors are hard to debug. If a selector needs more than two or three conditions, it's usually easier to click your way to a narrower part of the page first.


FAQs

What kind of selectors does Gaffa support?

Gaffa uses Playwright's selector engine, so you can use any standard CSS selector, Playwright's extensions for matching text and visibility, and XPath expressions.

How do I write a selector with quotes inside a JSON request?

Use single quotes inside the selector so the JSON string stays valid, like "input[name='email']". You can also escape double quotes with backslashes. Unescaped double quotes will break the request.

How do I select an element that has no ID or class?

Use a text selector like button:text('Submit'), or match on a stable attribute such as aria-label or data-testid. Both are more reliable than generated class names.

Can Gaffa select elements inside an iframe?

Same-origin iframes can be reached, so write your selector as normal, without targeting the frame first. Cross-origin iframes served from a different domain can't currently be accessed.

Why can't I click something inside an embedded video or payment widget?

Those are usually cross-origin iframes, served from a different domain to the page. Selectors can't currently reach inside them. Same-origin frames work normally.

Why can't I find an element in my DOM capture even though I can see it on the page?

It's probably inside an iframe. DOM captures return the main page only, showing iframes as empty tags. If the frame is same-origin, the element may still be selectable — try your selector directly.

What's the difference between :has-text() and :text()?

:has-text() matches any element containing that text, including parent elements. :text() matches only the smallest element directly containing it, which is usually what you want.

Last updated