Accessibility · ARIA Practice
The Role Attribute as a Contract With the Accessibility Tree
How a role token rewrites what the accessibility tree exposes, why it adds no behaviour, and how the first valid token in a list wins.
A <div> with a click handler and a <button> with a click handler run identical JavaScript when clicked. Only one of them is a control that assistive technology can find, name, and operate. The difference is not styling and not event wiring; it is the entry the browser writes into the accessibility tree, and the only way an author can rewrite that entry from markup is the role attribute. Understanding the attribute as a rewrite of a mapping, rather than as a label for behaviour, resolves most of the confusion around custom widgets.
The default mapping is per element, not per appearance
Before any ARIA is involved, the browser constructs an accessibility node for every DOM element that survives its own filtering. The element’s tag name, attributes, and content determine the role and the states of that node, following rules set out in the HTML Accessibility API Mappings document. A <button> becomes a node of role button with a press action; an <a> with an href becomes a link; an <h2> becomes a heading with level 2; an <li> inside a <ul> becomes a listitem.
Elements that carry no semantics of their own map to a generic container. Historically a <div> or <span> produced nothing at all in some trees, which is why old code could rely on a <div> being invisible to a screen reader. Current browser practice exposes generic containers as nodes with a generic role, mostly so that they can carry descriptions and relationships. The practical consequence is unchanged: a plain <div> tells a screen reader nothing about what it does.
The rendering is irrelevant at this stage. A <span> styled to look like a checkbox, with a background image for the tick, still maps to text. The tree is built from markup and from the platform semantics of form controls, and CSS participates only through properties that remove content entirely, such as display: none, which suppresses the subtree.
A role substitutes the node’s type, and nothing else
The role attribute lets an author replace the computed role with one from the WAI-ARIA vocabulary. role="button" on a <div> produces a node that assistive technology reports as a button, and the platform mapping that follows is the same one a real button gets: a UIA Button control type on Windows, an AXButton on macOS, a push button role over AT-SPI elsewhere. The Core Accessibility API Mappings document specifies that correspondence role by role.
What the attribute does not do is import behaviour. Focus, keyboard activation, form participation, and the disabled state are all properties of the element implementation, not of the role. The role is a claim about what the node means, and the browser passes the claim to assistive technology without checking it.
<!-- Announced as a button. Nothing else is true of it. -->
<div role="button" onclick="submit()">Save</div>
<!-- The element supplies focus, Enter and Space activation, and disabled state. -->
<button type="button" onclick="submit()">Save</button>
The gap is measurable. The <div> version is not in the tab order, so a keyboard user cannot reach it at all; it does not respond to Enter or Space; it has no disabled state that either the tree or the pointer honours. Each of those has to be rebuilt by hand, and every rebuilt part is a part that can drift out of sync with the visual rendering. This is the core trade of ARIA practice: the attribute is cheap, and the behaviour it implies is not.
Token lists, and the first valid token wins
The value of role is not a single token in the grammar; it is a space-separated list. A user agent walks the list and uses the first token it recognises, ignoring the remainder. role="switch checkbox" is a switch wherever switch is understood and a checkbox where it is not. The design exists so that new roles can be introduced with a fallback that older implementations still handle.
The fallback also means that errors are silent. A misspelled single token, role="buton", matches nothing, so the element keeps its implicit role and the typo produces no diagnostic anywhere: no console message, no validator complaint, because the attribute is valid HTML whatever it contains. A misspelled first token in front of a valid one, role="buton button", produces a button and hides the mistake from everyone, including the author who is trying to find out why a widget behaves oddly. The browser has no way to report the near miss, and the author has no signal other than a screen reader.
The vocabulary itself is organised as a taxonomy with a dozen abstract roles at the root: roletype, widget, composite, input, select, range, section, sectionhead, structure, landmark, window, and command. Abstract roles exist so that states and properties can be inherited without being repeated in every definition. They are not usable in documents. Writing role="widget" or role="input" yields no mapping, which is a quiet failure of a different kind: the value looks authoritative and does nothing.
Roles that require structure around them
Some roles are only meaningful inside a required context. option must be owned by a listbox; tab must be owned by a tablist; listitem must be owned by a list or a directory; row must be owned by a grid, table, or treegrid; menuitem must be owned by a menu or menubar. The constraint is not decorative. A screen reader that enters a listbox queries the count of options and the position of the current one; if the options are siblings of the listbox rather than children, the query returns zero, and the user hears an empty list with unexplained content after it.
Ownership is defined on the accessibility tree, not on the DOM, which is what makes aria-owns possible: an ID reference list that reparents nodes into the current element after the browser has built the tree. It is a repair mechanism with real cost, because the tree order and the DOM order now differ. Sequential reading follows the tree, so a reader may encounter content in an order that does not match the visual layout, and focus management through a reparented subtree stops matching the tab order. The narrower the use, the better: aria-owns to pull a single popup into a combobox is defensible; aria-owns to rebuild a layout is a sign the markup should change instead.
The related constraint is naming. Roles are defined with a nameFrom source, either author or contents. A button may take its name from its contents, which is why <button>Save</button> needs no aria-label. A textbox takes its name from the author, so a bare input with a placeholder is unnamed. A heading takes its name from contents. A img requires an author-supplied name, because there is no content for the name computation to fall back on. The practical rule: for any role whose definition says author, a name must be supplied explicitly, and for landmark roles the name is not optional in practice, since a region without an accessible name is not exposed as a landmark at all.
Where the substitution goes wrong
The first failure is a role that contradicts the element’s behaviour. A <button role="tab"> is announced as a tab, but the element still fires click on Enter and Space, while the tab pattern is built on arrow keys moving selection. A <a role="button"> keeps its link behaviour: Ctrl-click opens a new tab, the context menu offers link commands, and the status bar shows a destination that does not exist. Assistive technology exposes the role it was told and the actions the platform provides, and the two now disagree.
The second failure is a role that changes more than the author intends. role="application" tells the screen reader to stop intercepting keystrokes and pass them to the page, which is correct for a widget that implements a complete keyboard model and catastrophic for one that does not, since the user loses document navigation keys with no way back except leaving the region. The role is a statement about who owns the keyboard, and it should be scoped to the smallest subtree that genuinely handles keys.
The third failure is role="presentation" applied where it cannot take effect. The value, and its synonym none, removes only the element’s own semantics; children keep theirs. That is what makes it useful for a layout <table>: the table and cell semantics disappear, and the text remains. The specification forbids it on focusable elements and says it is ignored on any element carrying global ARIA states or properties, because removing the role of something that is focusable or stateful leaves a contradiction the tree cannot express. In practice the attribute is silently ignored in those cases, which produces a table that still announces as a table after an author has “fixed” it.
The last failure is the stale role. A widget whose role is set at build time and whose states are updated at runtime will be announced as a widget whose state is wrong, and the mismatch is harder to diagnose than a missing role. Role is the type of the node; the states and properties are the rest of the contract, as covered in mirroring widget state into ARIA properties. A tree that has been reparented rather than rewritten is discussed in custom widgets over virtualized content, where the same ownership rules apply to nodes that are not in the DOM at all.
References
- WAI-ARIA 1.2 — W3C
- ARIA in HTML — W3C
- Core Accessibility API Mappings 1.2 — W3C
- HTML Living Standard — WHATWG