Accessibility · Semantics and the A11y Tree
Mapping ARIA Roles onto Platform Accessibility APIs
How a role token becomes an MSAA role, an AXRole, or an AT-SPI role, which properties carry states, and where the mappings diverge.
No assistive technology reads ARIA. ARIA is a vocabulary for authors; what a screen reader consumes is whatever the operating system’s accessibility interface exposes, and those interfaces predate ARIA by a decade. On Windows the relevant interfaces are MSAA and its successor UIA; on macOS, the NSAccessibility protocol with AXRole and AXSubrole; on Linux, AT-SPI over D-Bus. Core-AAM is the document that maps each ARIA role and property onto each of these, and it is the layer where two platforms can legitimately disagree about the same markup.
The mapping is one-to-many, and it operates on the tree that the exposure rules produced, so a node that never reached that tree has nothing to map. A single role becomes a role constant plus a set of properties, and the same property may be a state, a relation, or a separate interface depending on the platform. A checkbox is ROLE_SYSTEM_CHECKBUTTON with STATE_SYSTEM_CHECKED on MSAA, AXCheckBox with AXValue on macOS, and ATK_ROLE_CHECK_BOX with an AtkState on AT-SPI. The state lives in a different place in each case, and the assistive technology queries it differently.
Roles that do not survive
Some ARIA roles have no counterpart at all. generic maps to nothing; objects with it are either exposed with a platform’s neutral role or omitted. none and presentation never reach the platform layer, because ignoring happens before the mapping. feed, math, note, strong, emphasis, subscript, superscript, term, definition, and time were added in ARIA 1.1 and 1.2, and their mappings range from a dedicated platform role to a text attribute to nothing.
When no role constant exists, the mapping usually falls back to the nearest structural equivalent and carries the difference in a property. feed maps to a group-like object with a specific object attribute, so a consumer that does not know the role still gets a container it can traverse. math maps to a platform role where one exists and otherwise to a generic object with a math attribute, which is why formula navigation depends on the screen reader rather than on the browser.
The practical rule is that a role’s existence in the ARIA specification does not imply that it is announced. A new role takes years to travel from specification to Core-AAM to engine to assistive technology, and each step can drop it. role="switch" and role="searchbox" are now broadly mapped, but were exposed as checkbox and textbox respectively for a period long enough that authors still write fallback token lists.
States travel as properties
ARIA states and properties (the aria- attributes that are not naming or description) map to platform properties, and the mapping is where most cross-platform inconsistency shows up.
aria-expanded becomes STATE_SYSTEM_EXPANDED or STATE_SYSTEM_COLLAPSED on MSAA, AXExpanded on macOS, and an EXPANDED state on AT-SPI. The tri-state is the problem: absent, false, and true are three conditions, but MSAA’s two flags can only express two. The absent case is encoded by omitting both flags, which is distinguishable only if the consumer checks for neither rather than for collapsed. A screen reader that tests “not expanded” reads every non-expandable object as collapsed.
aria-checked has the same shape with a third value, mixed. MSAA represents it as STATE_SYSTEM_MIXED alongside STATE_SYSTEM_CHECKED; macOS uses AXValue with a value of 2 for mixed and 0 or 1 otherwise; AT-SPI uses a separate state. An object that reports mixed but not checked is ambiguous, and the mappings differ on whether the checked flag accompanies mixed.
aria-valuenow, aria-valuemin, and aria-valuemax map to a range interface. On MSAA that is IAccessible::get_accValue as a string plus the value interface; on macOS a slider exposes AXValue, AXMinValue, and AXMaxValue as separate attributes. The string encoding matters: a range with a fractional step exposes a formatted string, and a consumer that parses it as an integer loses the fraction. A role that supports a value but omits the three attributes inherits defaults from its definition: 0 for the minimum, 100 for the maximum, and the current value for aria-valuenow, so a slider with no attributes reports a range of 0 to 100 whether or not that matches the quantity it controls.
aria-current is a good example of a property with no platform home. It maps to a text attribute or an object attribute in Core-AAM, not to a state, because no platform interface has a slot for it. A consumer that does not read object attributes cannot distinguish the current page’s link from its siblings. aria-sort, aria-keyshortcuts, and aria-roledescription are in the same category. They are exposed, but only to consumers that know where to look.
Relations become a separate graph
aria-labelledby, aria-describedby, aria-controls, aria-owns, and aria-flowto are relations. None of them is a property of a single object; each names another object. Every platform models this with its own relation mechanism: MSAA has no general relation interface and relies on a small set of named relations plus window messages, UIA has a set of property identifiers for labelled-by and described-by, macOS exposes AXTitleUIElement for the labelling target and AXHelp for description, and AT-SPI has an explicit relation set with typed relations.
aria-owns is the one relation that changes the tree rather than annotating it. It re-parents children for the accessibility tree without changing the DOM, which is how a combobox can expose a listbox that lives elsewhere in the document. The re-parented children are removed from their DOM parent’s object children and appended to the owner’s, so the platform tree no longer mirrors the DOM at that point. Cycles are rejected, and an element may be owned by only one parent. When the owner is removed, the children return to their DOM parent.
aria-owns is also the mechanism that repairs a widget whose required children are not its DOM descendants, a use covered in Required Owned Elements and Structural Constraints. The cost is that every consumer which cached a parent reference must handle the re-parenting, and the tree no longer corresponds to the visual layout. A screen reader’s touch exploration uses geometry, so an owned child positioned far from its owner produces an object that is a child in the tree and a stranger on screen.
// The accessibility tree parent, not the DOM parent, is what assistive
// technology traverses; aria-owns rewrites only the former.
const owner = document.getElementById('combo');
const list = document.getElementById('listbox');
owner.setAttribute('aria-owns', list.id);
// list is now a child of combo in the accessibility tree while remaining
// a child of body in the DOM.
Text and the absence of text
ARIA has no mechanism for marking up rich text semantics inside a single object. The platform interfaces carry text ranges with attributes: MSAA’s IAccessibleText, macOS’s AXAttributedString with attributes such as AXFont, AT-SPI’s text interface with an attribute run list. Core-AAM maps role="strong" and role="emphasis" to text attributes where the engine exposes them, but an engine that treats a strong as a generic inline element folds its text into the parent run and the attribute never appears.
This is the origin of the advice to use native strong, em, code, and blockquote rather than ARIA equivalents. The native elements have mappings that engines have implemented for longer, and the text attributes they produce are more likely to be exposed. The text layer itself, including how a run is split and how offsets are computed, is treated in Static Text Objects and the Text Interface.
Divergence as a permanent condition
Core-AAM specifies the mapping but not the timing, and platforms differ in what they can represent at all. A role with no constant gets an approximation; a property with no slot becomes an object attribute; a relation with no interface becomes a pointer. Three consequences follow for anyone debugging a mismatch between what the markup says and what a screen reader reports.
First, the reported role is often the fallback, not the requested role. A role="feed" announced as a group is not a bug in the browser; it is the mapping working as specified.
Second, the property that carries a state may be one the consumer does not query. An object can expose aria-sort correctly and still be announced without sort information.
Third, the platform layer is versioned independently of the browser. A new engine release can add a mapping that an older assistive technology does not read, so the same page produces different announcements on the same OS with a different screen reader build. Testing therefore has to name both the browser and the assistive technology, and a fix validated against one pair says nothing certain about the other.
References
- Core Accessibility API Mappings 1.2 — W3C
- WAI-ARIA 1.2 — Supported States and Properties — W3C
- Microsoft Active Accessibility (MSAA) — Microsoft documentation
- AT-SPI 2 — GNOME Accessibility project