Platform Details
xa11y normalizes platform-specific APIs into a unified interface. This page documents how roles and actions map across platforms, and where behavior differs.
Role mapping
Section titled “Role mapping”| xa11y Role | macOS (AX) | Linux (AT-SPI) | Windows (UIA) |
|---|---|---|---|
Window |
AXWindow, AXDrawer | window, frame | WindowControlType |
Application |
AXApplication | application | (synthesized per process; UIA has no Application accessible) |
Button |
AXButton | push button | ButtonControlType |
CheckBox |
AXCheckBox | check box, check menu item | CheckBoxControlType |
RadioButton |
AXRadioButton | radio button, radio menu item | RadioButtonControlType |
TextField |
AXTextField, AXSecureTextField | entry, password text | EditControlType |
TextArea |
AXTextArea | text | EditControlType |
StaticText |
AXStaticText | label, static, caption | TextControlType |
ComboBox |
AXComboBox, AXPopUpButton | combo box | ComboBoxControlType |
List |
AXList, AXOutline | list, list box | ListControlType, TreeControlType |
ListItem |
(via AXRow) | list item | ListItemControlType |
Menu |
AXMenu | menu (container) | MenuControlType |
MenuItem |
AXMenuItem, AXMenuBarItem | menu item, tearoff menu item; GTK menu with a native activation action |
MenuItemControlType |
MenuBar |
AXMenuBar, AXMenuBarExtra | menu bar | MenuBarControlType |
Tab |
(subrole AXTabButton) | page tab | TabItemControlType |
TabGroup |
AXTabGroup | page tab list | TabControlType |
Table |
AXTable | table, tree table | TableControlType, DataGridControlType |
TableRow |
(via AXRow) | table row | DataItemControlType (no cell signal) |
TableCell |
AXCell | table cell, column/row header | HeaderItemControlType, DataItemControlType (with TableItem pattern, or parent DataItem) |
Toolbar |
AXToolbar | tool bar | ToolBarControlType |
ScrollBar |
AXScrollBar | scroll bar | ScrollBarControlType |
Slider |
AXSlider | slider | SliderControlType |
Image |
AXImage | image, icon | ImageControlType |
Link |
AXLink | link | HyperlinkControlType |
Group |
AXGroup, AXScrollArea, AXRadioGroup | panel, section, form, scroll pane | GroupControlType, PaneControlType |
Dialog |
AXSheet (or subrole AXDialog) | dialog, file chooser | WindowControlType (with IsDialog) |
Alert |
(subrole AXApplicationAlert) | alert, notification | (alert pattern) |
ProgressBar |
AXProgressIndicator, AXBusyIndicator | progress bar | ProgressBarControlType |
TreeItem |
AXDisclosureTriangle (or subrole AXOutlineRow) | tree item | TreeItemControlType |
WebArea |
AXWebArea | document web, document frame | DocumentControlType |
Heading |
AXHeading (or subrole) | heading | (landmark pattern) |
Separator |
AXSplitter | separator | SeparatorControlType |
SplitGroup |
AXSplitGroup | split pane | PaneControlType |
Switch |
(subrole AXSwitch) | (inferred) | (inferred) |
SpinButton |
AXIncrementor | spin button | SpinnerControlType |
Tooltip |
AXToolTip | tooltip, tool tip | ToolTipControlType |
Status |
AXStatusBar | status bar | StatusBarControlType |
Navigation |
(landmark) | landmark, navigation | (landmark) |
Roles not recognized by a platform map to Unknown.
Action mapping
Section titled “Action mapping”| xa11y Action | macOS | Linux (AT-SPI) | Windows (UIA) |
|---|---|---|---|
Press |
AXPress | click, activate, press, invoke | InvokePattern |
Focus |
set AXFocused=true | Component.GrabFocus | SetFocus |
Blur |
set AXFocused=false | (not directly supported) | (not directly supported) |
Toggle |
AXPress (on checkbox) | toggle, check, uncheck | TogglePattern |
Expand |
AXShowMenu / AXPress | expand, open | ExpandCollapsePattern.Expand |
Collapse |
AXCancel / AXPress | collapse, close | ExpandCollapsePattern.Collapse |
Select |
AXPress | select | SelectionItemPattern |
SetValue |
set AXValue attribute | Value.SetCurrentValue (numeric) / EditableText.ReplaceText (text) | RangeValuePattern / ValuePattern |
TypeText |
set AXSelectedText | EditableText.InsertText | ValuePattern (splice) |
SetTextSelection |
set AXSelectedTextRange | Text.SetSelection | TextPattern range ops |
Increment |
AXIncrement | increment (or Value +step) | RangeValuePattern (+step) |
Decrement |
AXDecrement | decrement (or Value -step) | RangeValuePattern (-step) |
ShowMenu |
AXShowMenu | menu, showmenu, popup | ExpandCollapsePattern |
ScrollIntoView |
(no-op, no AX equivalent) | Component.ScrollTo | ScrollItemPattern |
Window verbs map differently again, because each platform’s window control API is separate from its element-action interface:
| xa11y Action | macOS | Linux X11 | Linux Wayland (Sway) | Windows (UIA) |
|---|---|---|---|---|
Activate |
activate app, restore if minimized, then AXRaise | _NET_ACTIVE_WINDOW |
Sway focus |
restore if minimized, then SetForegroundWindow and SetFocus |
Minimize |
clear AXFullScreen when fullscreen, then set AXMinimized=true | ICCCM WM_CHANGE_STATE(IconicState) |
unsupported | WindowPattern.SetWindowVisualState(Minimized) |
Maximize |
unsupported | _NET_WM_STATE_MAXIMIZED_HORZ + _VERT |
unsupported | WindowPattern.SetWindowVisualState(Maximized) |
EnterFullscreen |
set AXFullScreen=true | _NET_WM_STATE_FULLSCREEN |
Sway fullscreen enable |
unsupported |
Restore |
clear AXMinimized; set AXFullScreen=false | map and remove fullscreen/maximized EWMH states | Sway fullscreen disable |
WindowPattern.SetWindowVisualState(Normal) |
Close |
press the AXCloseButton | _NET_CLOSE_WINDOW |
Sway kill |
WindowPattern.Close |
MoveTo |
set AXPosition | _NET_MOVERESIZE_WINDOW |
Sway move position for floating windows |
TransformPattern.Move |
ResizeTo |
set AXSize | _NET_MOVERESIZE_WINDOW |
Sway resize set for floating windows |
TransformPattern.Resize |
Maximize and EnterFullscreen are distinct operations, not two names for
one state: macOS has no accessible maximize (see below) and Windows has no
fullscreen accessibility API, so each platform advertises only the operation
it can actually perform.
Window state reads follow the same split. minimized, maximized, and
fullscreen are Option<bool>: None means unknown, either because the
element is not a window or because the platform cannot report the state.
macOS reads minimized and fullscreen
(AXMinimized / AXFullScreen) on window-like elements; there is no AX
attribute for the zoom state (AXZoomed is not declared in the SDK, and live
windows answer kAXErrorAttributeUnsupported for it), so maximized is
always None on macOS. Entries into native fullscreen report through
fullscreen. Windows reads minimized and maximized from
WindowPattern.CurrentWindowVisualState but cannot report fullscreen;
Linux X11 reads all three states from _NET_WM_STATE. Sway reports
fullscreen; minimized and maximized stay unknown because Sway has no
such states. Other Wayland compositors retain the AT-SPI ICONIFIED reading
for minimized and leave the other states unknown. modal is
WindowPattern.CurrentIsModal on Windows and AXModal on macOS.
An advertised window verb means the current native backend reported support
and the accessibility target resolved to one native window. A missing verb is
unsupported or unresolved for that snapshot. A direct call returns
Unsupported with the missing capability or identity reason. Window states
are tri-state: Some(true) and Some(false) are observed values, while
None is unknown.
Linux snapshots also expose raw.window_capabilities, with one of
supported, unsupported, or unknown for each window verb. unknown means
no native backend or reliable identity was available. raw.window_backend
names the provider when resolution succeeded (x11-ewmh or
wayland-sway-wlr-foreign-toplevel). These raw fields are diagnostic; the
portable dispatch contract remains the actions list plus the method error.
On Sway, raw.window_protocol_version records the advertised wlr protocol
version used for capability detection.
X11 and Sway success means the native request was accepted for delivery. The
window manager applies requests asynchronously, so callers that require an
observed postcondition should fetch a new snapshot and wait for the state or
bounds. A compositor refusal or stale target is an error, as is ambiguous identity.
Maximized and fullscreen remain separate states. Restore clears every state
that the selected backend can represent.
Platform caveats
Section titled “Platform caveats”ScrollIntoView
Section titled “ScrollIntoView”macOS has no direct equivalent, so this action is a no-op. On Linux it uses Component.ScrollTo with top-edge alignment. On Windows it uses ScrollItemPattern.ScrollIntoView.
Activate: AXRaise alone does not activate
Section titled “Activate: AXRaise alone does not activate”AXRaise on a window only re-raises the window within the window list of its
own app, and it answers success even while the app sits in the background. An
activation performed on a background app’s window visibly does nothing.
Activate on macOS brings the app forward first (sets AXFrontmost on the
application element), then raises the window. A failure to activate is a
surfaceable error, never a silent success.
EnterFullscreen: native fullscreen on macOS
Section titled “EnterFullscreen: native fullscreen on macOS”macOS exposes native fullscreen through the AXFullScreen attribute, the
only window state the platform both reads and writes. EnterFullscreen
sets that attribute to true, and Restore sets it back to false. Because
an already-fullscreen window keeps advertising enter_fullscreen whether or
not AXFullScreen is settable, a repeated call stays a no-op. When the
window is not fullscreen and cannot be set fullscreen, the verb reports
ActionNotSupported instead of succeeding silently.
Maximize is a separate operation and is not supported on macOS. The
classic zoom state has no readable or writable accessibility surface:
AXZoomed is unsupported, and the green button’s AXPress / AXZoomWindow
actions are toggles with no readable state. The provider refuses the verb
with Unsupported and never advertises it. Substituting fullscreen
for a maximize request would blur two distinct operations. Windows is the
reverse: Maximize drives WindowVisualState_Maximized, while
EnterFullscreen is unsupported because UIA has no fullscreen surface.
The fullscreen transition is asynchronous, and macOS reports transient values
while it runs: a window entering fullscreen can briefly read
AXFullScreen=false, and a set issued mid-transition is discarded rather
than queued. The provider polls the window’s fullscreen state and re-issues
the absolute set until the promised state holds. A transition that never
commits fails with a timeout error rather than reporting success. A window
that is not fullscreen and whose AXFullScreen is not settable has no
fullscreen state to settle: Restore clears AXMinimized only and returns
without polling. A window that reads fullscreen but cannot clear the state
does not advertise restore at all.
Minimize on a fullscreen window leaves fullscreen first. AppKit ignores a
minimized set while the window is fullscreen, so issuing it directly would
report success while the window stayed fullscreen. The provider settles the
fullscreen exit first, then applies the minimized state. That is what makes a
back-to-back enter_fullscreen → minimize → enter_fullscreen →
minimize sequence land every step.
Leaving fullscreen is Restore, not a verb of its own. macOS has a real
“Exit Full Screen” command, but no distinct accessibility operation behind
it. The green button’s actions are toggles, and the only absolute exit is the
same AXFullScreen=false write that is the inverse of EnterFullscreen.
Restore performs that write, as well as AXMinimized=false, which makes it
the inverse of every entry verb. A separate exit_fullscreen verb would be a
second name for the same operation, advertised next to restore on the same
window. Minimized, maximized, and fullscreen are mutually exclusive on macOS:
a fullscreen window cannot be minimized, and entering fullscreen brings the
window back from the Dock first. Restore has one effect from any state.
SetValue: text vs numeric
Section titled “SetValue: text vs numeric”- macOS sets the
AXValueattribute directly, which works for both text and numeric values. - Linux has a
ValueD-Bus interface that only supportsf64, so text values requireEditableText.ReplaceText. If neither interface is available, the call returnsTextValueNotSupported. - Windows tries
RangeValuePatternfor numeric values and falls back toValuePattern.SetValuefor text.
TypeText
Section titled “TypeText”Text is inserted through the accessibility API. Keyboard events are never simulated.
- macOS sets
AXSelectedText, replacing the selection or inserting at the cursor. - Linux calls
EditableText.InsertTextat the current caret offset. - Windows reads the current value via
ValuePattern. It then splices in the new text and callsSetValue.
- macOS sets
AXFocusedto false on the element. - Linux and Windows have no direct API equivalent, so the action is unsupported.
Coordinates
Section titled “Coordinates”Bounds use desktop coordinates with origin at the top-left of the primary display. Windows uses physical pixels. macOS and Linux use logical points where available. Use the screenshot coordinate methods to map bounds onto captured pixels. Note:
- macOS reports in points, its native logical unit. On Retina displays, 1 point = 2 physical pixels.
- Windows reports physical desktop pixels for bounds, input points, window moves and sizes, and capture regions.
Screenshot.scaleis1.0. A window crossing a display seam has continuous bounds and its center remains inside the window, regardless of each display’s DPI scaling. Code that stored Windows DIP coordinates must convert them to physical desktop pixels before passing them to xa11y. - Linux reports logical pixels where the HiDPI scale is reliably detectable, and falls back to physical at scale
1.0otherwise. See Coordinates and HiDPI for exactly when. - Multi-monitor setups can produce negative coordinates for displays positioned left of or above the primary.
Name resolution
Section titled “Name resolution”Platforms use different attributes to determine an element’s name:
- macOS uses AXTitle, then AXDescription, then AXValue (for text elements).
- Linux uses Accessible.Name, then Description. For
StaticTextwith no name, the first portion of the value is used. - Windows uses the CurrentName property.
Application nodes (Windows)
Section titled “Application nodes (Windows)”UI Automation has no Application accessible. Processes surface only as
their top-level windows, so xa11y-windows synthesizes one Application
node per process (the unified shape every platform reports), with the
process’s windows as its children. The node carries no bounds (UIA has no
process geometry) and no window actions or window-state flags. Its handle
is a tagged synthetic identifier and backs no live UIA element, so acting
on the app node itself is refused with ActionNotSupported (the core role
guard rejects it before any provider is reached, on every platform);
enumerate the windows and act on a window child instead. The handle is
not PID-encoded: the PID is the node’s pid field, and the handle itself
is a per-run counter tagged as synthetic.
The node’s raw map records how it was built:
| raw key | Value |
|---|---|
uia_synthesized |
true. The marker for a node that was synthesized and backs no live UIA element. |
uia_name_source |
"process" (executable stem from OpenProcess + QueryFullProcessImageNameW), or "window_title" (the representative window’s title, used when the process cannot be opened) |
uia_process_name |
full executable path, when the process could be queried |
The representative window is the process’s first top-level window in z-order.
Windows subscriptions register their UIA handlers on every current top-level
window of the process, and a desktop-scoped open/close watch attaches windows
opened after subscribe (and drops closed ones, so a closed window stops
producing state deltas). Events from sibling windows follow the
same “the process” semantics as the main window.
Checked state
Section titled “Checked state”The tri-state checked value (Off / On / Mixed) is derived differently:
- macOS parses the
AXValueattribute, where"0"= Off,"1"= On,"2"= Mixed. - Linux uses the AT-SPI state bits
Checked(0x10) andMixed(0x2000). - Windows reads
TogglePattern.CurrentToggleState, where 0 = Off, 1 = On, 2 = Indeterminate.
Selected state
Section titled “Selected state”Per-item selection (selected on rows, cells, list items) is read differently:
- macOS uses the per-element
AXSelectedattribute when present. Some bridges (Qt’s among them) provide no per-elementAXSelectedand expose selection only through the container’sAXSelectedChildren. Fortable_cell/table_row/list_itemelements whoseAXSelectedattribute is absent, xa11y resolvesselectedby membership in the nearest ancestor’sAXSelectedChildren(at most two hops: cell → row → table). Derived values appear instates.selectedalone, andrawnever gains a syntheticAXSelectedkey. - Linux uses the AT-SPI
Selectedstate bit. - Windows uses
SelectionItemPattern.CurrentIsSelected.
Table headers
Section titled “Table headers”Header exposure varies by toolkit as well as platform. Windows surfaces header cells as HeaderItem elements and per-cell header relationships via the TableItem pattern. AT-SPI exposes column header / row header roles. AppKit tables expose a header group whose sort buttons carry the column titles. One known toolkit gap: Qt tables on macOS expose no header objects at all. Qt’s Cocoa bridge synthesizes AXRow/AXColumn children only and provides no AXHeader attribute, so column titles are absent from the AX tree and xa11y cannot surface them. This is an upstream Qt limitation. Use the platform-consistent table table_cell selectors for data, and avoid depending on header names for Qt apps on macOS.
Linux: X11 vs Wayland
Section titled “Linux: X11 vs Wayland”xa11y supports both X11 and Wayland Linux sessions. Backends are selected at runtime, with no compile-time feature flag.
| Capability | X11 | Wayland |
|---|---|---|
| Accessibility (AT-SPI2) | D-Bus, no display-server dependency | D-Bus, no display-server dependency |
| Input simulation | XTest extension (no setup) |
/dev/uinput virtual evdev device (requires input group) |
| Screen capture | GetImage on the root window |
PNG URI from org.freedesktop.portal.Screenshot |
| Window management | EWMH/ICCCM | Sway native IPC, gated by zwlr_foreign_toplevel_manager_v1 |
Auto selection uses these rules independently for input, screenshots, and window management:
- A usable
XDG_SESSION_TYPEidentifies the desktop session. - When both display variables are present, xa11y selects Wayland because
DISPLAYnames the XWayland compatibility endpoint in that configuration. - A stale session type falls back to the live endpoint.
- No endpoint returns an
Unsupportederror that explains the missing endpoint.
Set XA11Y_LINUX_INPUT_BACKEND, XA11Y_LINUX_SCREENSHOT_BACKEND, or
XA11Y_LINUX_WINDOW_BACKEND to x11 or wayland for an explicit route.
auto restores detection. Each override is independent, and a missing
endpoint is an error instead of a fallback to another backend.
Native window identity and support
Section titled “Native window identity and support”X11 resolves AT-SPI windows against _NET_CLIENT_LIST. It requires an exact
_NET_WM_PID match. For a process with multiple windows, live frame geometry
and then the title narrow the candidates within that PID. Title is never identity on its own.
Every action resolves again, which catches destroyed windows and XID reuse.
The initial native Wayland backend targets Sway. It requires Sway’s IPC socket and the compositor’s advertised wlr foreign-toplevel protocol. The wlr protocol has no PID or accessibility identifier, so xa11y uses the Sway tree to prove PID-backed identity and to issue the compositor request. That tree contains native Wayland and XWayland toplevels, and duplicate candidates fail as ambiguous. Other compositors such as GNOME and KDE Plasma currently report native window actions as unsupported. They do not receive simulated shortcuts.
Sway supports activation, fullscreen entry and exit, and close. Its model has
no minimized state or maximized state distinct from fullscreen. Move and resize
are advertised only for floating containers. X11 capabilities come from the
window manager’s _NET_SUPPORTED list. Linux window-state events still come
from AT-SPI; EWMH and Sway state changes without a corresponding AT-SPI event
must be observed by reading a new snapshot.
Why uinput instead of libei + portal RemoteDesktop?
Section titled “Why uinput instead of libei + portal RemoteDesktop?”org.freedesktop.portal.RemoteDesktop (libei) is the architecturally “correct” Wayland input-sim path and the future of structured input injection on Linux, but today it covers a strict subset of the ecosystem: only xdg-desktop-portal-gnome and xdg-desktop-portal-kde support it, and neither has a usable headless mode without a session manager (GDM / SDDM). uinput goes through the kernel and is compositor-agnostic, so it works on every Wayland desktop (GNOME, KDE Plasma, sway, Hyprland, Cosmic, weston) and on headless Linux servers, with the same code path. It is the same mechanism xdotool --using-uinput, ydotool, wtype, Steam Input, and Wine all use.
The trade-off is that uinput requires the user to be in the input group, which grants global input read/write, comparable to macOS Input Monitoring. The portal model is finer-grained (per-app consent) but isn’t broadly available yet. xa11y may add a libei backend later as an opt-in upgrade once the portal landscape stabilises.
Wayland screenshot portal consent
Section titled “Wayland screenshot portal consent”The Screenshot portal usually auto-approves for non-interactive callers (interactive=false, modal=false); xa11y passes both. wlroots compositors via xdg-desktop-portal-wlr and GNOME via xdg-desktop-portal-gnome both support this.
Coordinates and HiDPI
Section titled “Coordinates and HiDPI”Element.bounds and input Points are logical (device-independent) coordinates, matching the cross-platform contract, and Screenshot.scale reports the physical-to-logical ratio. The scale is detected best-effort and is exact where it can be read reliably:
- X11, integer scaling. Read from
Xft.dpiin the XRESOURCE_MANAGER(the same signal GTK/Qt use). AT-SPI extents andXTestare physical, so bounds are divided and input points multiplied by it. - Single-output Wayland, integer or fractional. The exact ratio
physical_mode / logical_sizefromxdg-output(e.g.1920 / 1280 = 1.5), rather than the roundedwl_output.scale.
Every other configuration fails closed to 1.0, so the data is never wrong. There, bounds stay physical and the scale is not upscaled, but capture and input round-trips remain correct because bounds and pixels stay in the same space:
- Fractional X11. GTK’s X11 window scale is integer-only, so a fractional
Xft.dpiscales fonts, not the coordinate space. - Multi-monitor mixed-DPI Wayland. A single scalar can’t represent per-monitor scales, so it falls back to the largest integer
wl_output.scale. - XWayland in a Wayland session. Auto selection uses the Wayland screenshot portal and output scale. An explicit screenshot override of
x11uses the X11 source, which stays1.0under XWayland.
Point arguments to input simulation, after the logical→physical conversion, are mapped onto the uinput virtual coordinate range. Override the assumed screen size (default 1920×1080) with XA11Y_SCREEN_WIDTH / XA11Y_SCREEN_HEIGHT env vars if your setup differs.
GTK press fallback (Linux)
Section titled “GTK press fallback (Linux)”GTK 4 menu-button widgets (GtkMenuButton, AdwMenuButton, AdwSplitButton) present as an outer push button accessible that advertises NActions = 0 wrapping an inner toggle button that carries the real click action. Calling press() on the outer would normally raise ActionNotSupported. When the owning application identifies itself as GTK via Application.ToolkitName == "GTK", xa11y-linux instead walks a bounded slice of the outer’s subtree (BFS, depth 3, restricted to roles that carry a click action, name must match) and invokes the single matching descendant it finds. Scoping is strict. It only runs inside GTK apps, only when the widget’s own Action interface is empty, and only when exactly one candidate matches. Ambiguous subtrees still surface the original error.
WebKitGTK tables
Section titled “WebKitGTK tables”Two WebKitGTK behaviors affect <table> content (verified against WebKitGTK 2.52; both are engine behaviors, not xa11y ones):
<th>cells can destabilize the whole page’s tree. With a window manager present, a table containing<th>header cells can send WebKit’s accessibility tree into continuous invalidation churn. Every content accessible goes defunct moments after being exposed, and the page effectively vanishes from AT-SPI. If a WebKitGTK-based app’s tree keeps coming back empty orunknown, check for this (xa11y marks dead objects withraw["atspi_defunct"]).- Layout-table heuristic. A
<table>without headers or a<caption>is judged decorative and dropped from the accessibility tree entirely. Cell text is exposed through the AT-SPI Text interface (surfaced as the cell’svalue) rather than as the accessible name, so usetable_cell[value*="..."]selectors for WebKitGTK content.
Linux Electron and Chromium apps
Section titled “Linux Electron and Chromium apps”On Linux, Electron and Chromium-based apps have their AT-SPI2 bridge disabled by default for performance. xa11y will connect to the app but its tree will contain only the top-level window, so App.by_name("…").locator("button").count() returns 0 even though buttons are visible on screen.
To expose the full tree, launch the app with --force-renderer-accessibility:
# VS Codecode --force-renderer-accessibility
# Cursorcursor --force-renderer-accessibility
# Google Chrome / Chromiumgoogle-chrome --force-renderer-accessibilityRepresentative node counts on Ubuntu 24.04 + GNOME 46 (Wayland):
| App | Without flag | With flag |
|---|---|---|
| VS Code | 1 | 140 |
| Cursor | 1 | 116 |
| Chrome | 1 | 210 |
Native GTK apps (Nautilus, gnome-terminal, GNOME Calculator, gnome-text-editor) don’t need the flag, because their AT-SPI2 bridge is enabled by default.
Firefox exposes its tree when launched with MOZ_ACCESSIBILITY_ATK2=1 set in the environment.
To diagnose whether a target app has AT-SPI2 enabled at all:
busctl --user tree org.a11y.atspi.Registry | grep -i "<app-name>"If the app’s subtree is missing, the problem is the app’s accessibility configuration, not xa11y.