Skip to content

Selectors

A selector is the query string passed to app.locator(selector), to the module-level xa11y.locator(selector), and to the xa11y find and xa11y action CLI commands. It selects elements for actions, waits, and queries.

This page is the syntax. For the workflow of arriving at a selector that matches, see Find the right selector.

button # by role
button[name='Submit'] # role + exact name match
text_field[name^='Search'] # starts-with (case-insensitive)
text_field[name*='email'] # contains (case-insensitive)
button[name$='Cancel'] # ends-with (case-insensitive)
[focusable='true'] # attribute only, any role
* # universal selector, any element
group > * # every direct child, whatever its role
group > button # direct child combinator
window button[name='OK'] # descendant (any depth)
button:nth(2) # 2nd match (1-based)
button[name='Play'], button[name='Pause'] # alternation

A selector may begin with a role name. Normalized roles are written in snake_case:

button, text_field, text_area, check_box, radio_button, static_text, combo_box, list, list_item, menu, menu_item, menu_bar, tab, tab_group, table, table_row, table_cell, toolbar, scroll_bar, scroll_thumb, slider, image, link, group, dialog, alert, progress_bar, tree_item, web_area, heading, separator, split_group, switch, spin_button, tooltip, status, navigation, window, application, unknown.

The mapping from each of these onto the platform’s own role vocabulary is in Platform Details. An element whose platform role has no normalized equivalent reports unknown, and unknown matches those elements.

A role name that is not a known normalized role is matched against the platform’s original role string instead, which is the escape hatch for selecting on a distinction normalization discards. AXButton and AXTabButton on macOS are examples. The comparison is exact and runs against the ax_role, ax_subrole, atspi_role, and class_name keys alone, so a value matching some other raw attribute cannot be mistaken for a role. Platform role names containing a space (AT-SPI’s push button) cannot be written this way, because a role token accepts only the characters [A-Za-z0-9_].

The role may be omitted entirely, in which case the selector matches any role: [name='Submit'] matches an element with that name whatever its role.

* matches any element. It means the same thing as omitting the role, so *[name='Submit'] and [name='Submit'] are the same query. Write it where a role cannot simply be left out:

Selector Matches
* Every element in scope
dialog > * Every direct child of a dialog, whatever its role
window * Every descendant of a window
group > * > button Buttons exactly two levels below a group

The last row is the case with no other spelling: it skips exactly one generation without naming the role in between. For the two middle rows, children() and tree() answer the same question imperatively when you already hold the parent element.

* binds as a whole segment, so it cannot be combined with a role name. *button, button*, and ** are all syntax errors.

A selector scoped to an application walks that application’s subtree. * retains every node of that walk instead of the few that match a role, so it materializes every element’s attributes:

  • On Windows the walk already materializes every node in one batched FindAllBuildCache call, so * costs nothing extra.
  • On macOS and Linux the walk fetches only the attributes a selector asks about, so * shifts the cost from “probe every node, build the matches” to “build every node.”

A bare * with no application scope repeats that across every running application. That is the same order of work as calling App.dump() on everything on the desktop. It is a reasonable thing to ask for when exploring, but prefer to scope the search or pass a limit in code that runs often.

The form is [attribute<operator>'value']. The value must be quoted with single or double quotes. Several filters may be chained, and all of them must match: button[name='OK'][enabled='true'].

Operator Meaning Case
= Exact match Case-sensitive
*= Contains Case-insensitive
^= Starts with Case-insensitive
$= Ends with Case-insensitive

There is no presence-only filter. [focusable] is a syntax error. Write [focusable='true'] instead.

The case-insensitive operators compare after a simple Unicode lowercase mapping rather than full case folding, so pairs such as Turkish I/ı and German ß/SS do not match. For names that differ only in such an edge case, use the case-sensitive = operator with the precise string.

Element properties, matched against the element’s normalized data:

Attribute Value format
role The snake_case role name
name String
value String
description String
bounds JSON object with keys in alphabetical order: {"height":…,"width":…,"x":…,"y":…}. An exact = match has to reproduce that order, so *= is the practical operator here.
numeric_value Number formatted as a string
min_value Number formatted as a string
max_value Number formatted as a string
stable_id String

State flags, matched as the strings 'true' and 'false':

Attribute Notes
enabled
visible
focused
active
focusable
selected
editable
modal
required
busy
expanded Absent on elements with no expand/collapse concept, which then match nothing

One tri-state flag:

Attribute Values
checked 'on', 'off', 'mixed'

Any other attribute name is looked up in the element’s platform-specific raw map, so [AXRoleDescription='slider'] on macOS or [atspi_name='…'] on Linux matches against the untouched platform value. The keys available there are platform-specific and listed under name resolution.

Combinator Meaning
A B (space) B at any depth below A
A > B B as a direct child of A

button:nth(2) selects the second match, 1-based, in document order. It applies to the segment it is attached to.

Requesting an index past the end produces a diagnosis naming both numbers, for example selector matched 2 element(s); nth(5) requested.

A top-level comma separates alternation clauses, with the same semantics CSS uses for selector lists. The result is the union of each clause’s matches, deduplicated by element identity and returned in document order.

This is useful when the label of an element changes with state and you want one locator that handles every variant:

# macOS Calculator's leftmost button is "All Clear" when the display is 0
# and "Clear" once you've typed anything. Match either.
app.locator("button[name='All Clear'], button[name='Clear']").press()

Each clause is parsed independently, so combinators apply per clause. window button, dialog button reads as “buttons under a window, plus buttons under a dialog”, rather than as a stray comma inside one selector.

Chained descendant() and child() calls on a group locator distribute over every clause:

# Equivalent to: "toolbar button, dialog button"
app.locator("toolbar, dialog").descendant("button")

Commas inside quoted attribute values ([name='a,b']) are not separators.

A malformed selector raises InvalidSelector at parse time, before any platform call, with a message naming the problem (expected operator (=, *=, ^=, $=), unterminated string in attribute value, empty simple selector).

A well-formed selector that matches nothing raises SelectorNotMatched from fail-fast calls, or Timeout from waits and auto-waiting actions. Both carry a diagnosis listing near-miss candidates. See Errors for the variants and Errors and diagnosis for how to read them.