Accessibility Tree
Every page Chrome renders has a second tree beside the DOM: the accessibility tree, the semantic view that screen readers and other assistive tech consume. Chrome computes it from WAI-ARIA△ roles and the accessible name◑ algorithm: implicit roles are resolved, names are computed, and aria-hidden or display:none nodes are pruned. VoidCrawl exposes it through the CDP Accessibility domain○.
The payoff is durability. A control identified as button "Load more" keeps that identity across a redesign that rewrites every class name and wrapper div. The accessibility tree is the markup-independent analogue of the DOM: query and click by what an element is, not where it sits in the HTML.
Reading the tree
There are two views, both on Page and PooledTab. Start with the outline.
ax_tree_outline(depth=None) returns a compact, indented role "name" string with text-noise and hidden nodes pruned. This is the view to read, whether you are an engineer eyeballing a page or an agent deciding what to click.
import asynciofrom voidcrawl import BrowserPool, PoolConfig
async def main() -> None: async with BrowserPool(PoolConfig()) as pool: async with pool.acquire() as tab: await tab.goto("https://qscrape.dev") print(await tab.ax_tree_outline())
asyncio.run(main())document navigation link "Home" link "Docs" main heading "qScrape" button "Get started"get_full_ax_tree(depth=None) returns the raw CDP nodes as a flat list of dicts, linked by childIds and parentId. Reach for this when you need the structured data rather than a readable summary. Each node carries:
| Field | Meaning |
|---|---|
role | AXValue-wrapped role, e.g. {"value": "button"}. |
name | AXValue-wrapped computed accessible name. |
properties | State such as focusable, expanded, checked. |
childIds / parentId | Tree links between nodes. |
backendDOMNodeId | The bridge back to the DOM element. |
ignored | Whether the node is excluded from the AX tree. |
depth bounds how far descendants are walked on either call; None returns the whole tree.
Querying for nodes
query_ax_tree(role=None, name=None) is the AX analogue of query_selector_all: it returns matching nodes addressed by semantics instead of markup. Name matching is exact against the browser’s computed accessible name, and role accepts any ARIA role◇. Passing neither argument returns every node under the document root.
# Every actionable control on the pagebuttons = await tab.query_ax_tree(role="button")print([b["name"]["value"] for b in buttons])
# A specific controlmatches = await tab.query_ax_tree(role="link", name="Docs")Acting on a node
click_by_role(role, name, nth=0) is the durable counterpart to clicking a CSS selector. It resolves the match through Accessibility.queryAXTree, picks the nth non-ignored node (zero-based), bridges to the DOM via backendDOMNodeId, scrolls it into view, and clicks. It raises if no such node exists.
await tab.click_by_role("button", "Load more")
# Third "Add to cart" button on the pageawait tab.click_by_role("button", "Add to cart", nth=2)When several elements share a role and name, query first to see how many match, then index with nth.
When the tree is thin
Not every page has good semantics. A div-soup app with few roles produces a sparse AX tree, and role-based navigation will be unreliable on it. The signal is the ratio of named nodes to total nodes: when it is low, the page is telling you to fall back to CSS selectors, a screenshot, or raw HTML. The MCP session_ax_tree tool surfaces this ratio directly as named_count versus node_count.
From an MCP agent
The same capability is available to Claude Code and other MCP clients through two tools on the MCP server:
session_ax_treereturns the outline (mode=compact, default) or raw nodes (mode=raw), plusnode_countandnamed_countso the agent can judge AX richness before relying on it.click_by_roleclicks byroleandname, with an optionalnth, as the markup-independent alternative to theclicktool.
See the MCP Server guide for the full tool list and the fallback chain to click and click_visual_coords.
Examples
examples/accessibility_tree.py— snapshot the full AX tree and reconstruct the hierarchy.examples/accessibility_navigation.py— list actionable controls and drive the page withclick_by_role.
FAQs
When should I use the AX tree instead of CSS selectors?
Reach for it when a page has good semantics but volatile markup, the hashed class names and deeply nested wrappers of most React and Tailwind apps. A role plus accessible name like button "Load more" survives a redesign that would break a CSS selector. Use CSS when the page is markup-stable or its AX tree is thin.
Why does get_full_ax_tree return almost nothing?
Two common causes. Either the page has not finished rendering (the AX tree only reflects real content after JavaScript runs, so call it after navigation settles), or the page genuinely has poor semantics, lots of div soup and few roles. Compare named_count to node_count: when the ratio is low, fall back to HTML, a screenshot, or CSS selectors.
What is backendDOMNodeId for?
It is the bridge from an AX node back to its DOM element. click_by_role uses it internally to resolve the matched accessibility node to the real element, scroll it into view, and click it. You rarely need to touch it directly.
How does click_by_role disambiguate when several elements share a role and name?
It clicks the nth non-ignored match (zero-based, defaulting to the first). If a page has three button "Add to cart" controls, pass nth=2 for the third. Use query_ax_tree first to see how many match.
References
△ WAI-ARIA 1.2. W3C. Roles, states, and properties that define the accessibility tree. https://www.w3.org/TR/wai-aria-1.2/
○ CDP Accessibility domain. Chrome DevTools Protocol. getFullAXTree and queryAXTree, the calls VoidCrawl wraps. https://chromedevtools.github.io/devtools-protocol/tot/Accessibility/
◑ Accessible Name and Description Computation. W3C. How the browser computes the accessible name matched by query_ax_tree and click_by_role. https://www.w3.org/TR/accname-1.2/
◇ ARIA roles. MDN. Reference for the role values you pass to query_ax_tree and click_by_role. https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles