Skip to content

Element Selectors

Element selectors let a script describe the interface element it needs. Their syntax is similar to CSS: start with an element type, then narrow the result by title, state, or hierarchy.

const buttons = root.queryElements('Button[title="Save"]');

In most cases, begin by selecting the target in Element Menu Mode. Open the Copy submenu and choose Copy Selector. Paste that result into your script, then simplify it for the way you plan to use it.

Cursor Crane generates a selector that aims to identify the current element precisely. Interface titles, list contents, and web text can change, though. After copying a selector, run it once and remove unstable parts until only useful distinguishing conditions remain.

For example, this selector is very specific:

Window[title="Project Alpha"] Group Button[title="Run"]

If the project name changes often and the Run button in its group is already distinctive, you can simplify it to:

Group Button[title="Run"]
GoalPatternExample
Find by element typeButton, TextField, ListButton
Find by title[title="..."]Button[title="Save"]
Find by identifier#idButton#save-button
Find by accessibility role without a Kind[role="AX..."][role="AXWebArea"]
Find by state:enabled, :focusedTextField:focused
Find any element**[visible=true]

The built-in Kind names are Application, Unknown, Popover, Group, Button, TextField, Cell, Image, Toolbar, ScrollArea, OverflowedWebContent, List, Link, and Draggable. A Kind is Cursor Crane’s internal classification and does not necessarily correspond to a single accessibility role. In a selector’s type position, a token can match either the element’s Kind or its accessibility role; accessibility roles may be written with or without the AX prefix, so Button matches AXButton.

Not every accessibility element has one of those Kinds. You can use any raw accessibility role in the type position, such as WebArea or AXWebArea, or omit the type entirely and filter by role:

WebArea
AXWebArea
[role="AXWebArea"]

Use * when you want an explicit wildcard, for example *[role="AXWebArea"]. Start with a copied selector when you are not sure which Kind or role to use.

A space means “anywhere inside,” > means “directly inside,” and a comma means “match either”:

Window Button
Window > Button
Button[title="Save"], Button[title="Cancel"]
  • Window Button finds buttons at any depth inside a window.
  • Window > Button finds only direct child buttons of a window.
  • The final line finds both Save and Cancel buttons.

Attribute conditions support exact, starts-with, ends-with, and contains matching:

PatternMeaningExample
=Matches exactlyButton[title="Save"]
^=Starts withStaticText[value^="Error"]
$=Ends withTextField[title$="name"]
*=ContainsButton[label*="Continue"]
AttributeAlso acceptsMatches
ididentifier; #id shorthandThe accessibility identifier, or the DOM identifier when no accessibility identifier is present.
domIddomIdentifierThe DOM identifier.
roleThe raw accessibility role, such as AXButton.
subroleThe raw accessibility subrole.
titleThe element title.
labeldescriptionThe accessibility label or description.
valueThe element value.
typeThe element type.
enabledWhether the element is enabled.
visibleWhether the element is visible.
hiddenWhether the element is hidden.
focusedWhether the element has focus.
domClassThe DOM class tokens; see below.

Wrap text containing spaces in single or double quotes.

Button[enabled=true]
TextField[focused=true]
StaticText[value*="Completed"]

domId (also spelled domIdentifier) and domClass are special DOM attributes. They are normally available only for content exposed by a browser, webview, or Electron app; native macOS controls generally do not provide them. Use them when a copied selector shows that the attribute is available.

*[domId="compose-button"]
Button[domIdentifier="compose-button"]

domClass exposes a DOM class list. Use it as an explicit attribute; CSS-style class shorthand is not supported.

Button[domClass="primary active"]
Button[domClass*="primary active"]
  • = requires exactly the same set of DOM class tokens. Their order does not matter.
  • *= requires the element to contain every listed DOM class token.
  • Button.primary and Button[class="primary"] are invalid forms; use Button[domClass="primary"] instead.

DOM class values are split on whitespace before matching, so menu does not match a menu-item class. Copied selectors include domId and domClass when those attributes are available on the selected element.

PatternMeaning
:firstTakes the first matching result.
:nth(n)Takes the nth result, starting at 1.
:enabled / :disabledKeeps enabled or disabled elements only.
:visible / :hiddenKeeps visible or hidden elements only.
:focusedKeeps the currently focused element only.
List Row:nth(2) Button:first

This finds the second row in a list, then takes the first button in that row.

Element queries are not especially cheap: queryElements() traverses children and can block noticeably for a large accessibility hierarchy. Avoid repeating the same query. When several targets share a parent, query that parent once, then query each target from the parent instead of searching the entire window every time. For long lists, tables, and scroll areas, queryVisibleElement() is better when you only care about the content a person can see right now:

const visibleButtons = root.queryVisibleElement('Button:enabled', 50);
const toolbar = root.queryElements('Toolbar[title="Formatting"]', 1)[0];
const boldButton = toolbar?.queryElements('Button[title="Bold"]', 1)[0];
const italicButton = toolbar?.queryElements('Button[title="Italic"]', 1)[0];

Both methods accept an optional second argument that limits how many results they return.

Find the window first, then search from that window. The bundled example.element-query.js demonstrates the complete flow:

const application = ctx.elementInspector.getElementByPid(window.application.pid);
const root = application?.getWindows().find(
element => element.windowId === window.windowId
);
const matches = root?.queryVisibleElement('Button[enabled=true]', 50) ?? [];

Querying elements requires "accessibilityAPI" in meta.permissions. After finding an element, you can highlight it, move to it, set its value, or trigger one of its supported actions. See Script API for details.