Recursive AI

Roles: the WAI-ARIA data and the role computation

All onehtml docs

RecursiveAI.OneHtml.Accessibility computes the role of every element from markup alone, before any style is known. This document is normative for that part: the data it is built from, the algorithm, and what it reports. Choices the specifications leave open are in accessibility-chromium-profile.md.

Data

The taxonomy is generated by tools/aria/generate-aria.py from specification documents pinned in tools/aria/pins.json (URL, title, SHA-256 of the document as retrieved on 2026-10-25). tools/aria/fetch-specs.py CACHE downloads them and checks the hashes; generate-aria.py CACHE rewrites the JSON under src/OneHtml.Accessibility/Aria/Data/ byte for byte reproducibly. The files are embedded in the assembly and read with System.Text.Json; each has a version field (currently 1).

SourceUsed for
WAI-ARIA 1.2, W3C Recommendation, 6 June 2023roles, states and properties
Digital Publishing WAI-ARIA Module 1.1, W3C Recommendation, 12 June 2025the doc-* roles
WAI-ARIA Graphics Module 1.0, W3C Recommendationthe graphics-* roles
HTML Accessibility API Mappings 1.0, W3C Working Draft, 21 October 2026element and attribute mapping tables (text extract)
WAI-ARIA 1.3, W3C Editor's Draft, 21 October 2026only what browsers already ship and the 1.2 text lacks (below)

Draft additions. Three roles, comment, mark and suggestion, and five properties, aria-braillelabel, aria-brailleroledescription, aria-description, aria-colindextext and aria-rowindextext, come from the 1.3 draft (module Aria13Draft). mark is needed by HTML-AAM's mark element. The draft properties are added to the supported lists of the roles they apply to (the first three to every role). image is not a role of its own: it is treated as the preferred synonym of img, as the draft and Core-AAM's computed role do. The draft's sectionheader and sectionfooter roles are not included (see the profile entry section-header-footer).

Counts

Roles141: WAI-ARIA 1.2 94 (12 abstract), Digital Publishing 41, Graphics 3, 1.3 draft 3
States and properties53: 10 states and 43 properties; 20 are global
HTML-AAM element entries147
HTML-AAM attribute entries162

Role record

For every role: name, module, abstract, superclasses, requiredContext, requiredOwned (each entry a chain such as ["group", "option"]; any chain satisfies the requirement), required, requiredWhenFocusable (separator's aria-valuenow), supported (own, required and inherited, flattened), deprecated (the supported ones the specification marks deprecated on that role), prohibited, nameFrom (author, contents, prohibited), nameRequired, childrenPresentational and implicit (default values, such as aria-live polite for log). none shares the characteristics of presentation; the data of presentation is used for both.

One entry is added to the generated data by tools/aria/generate-aria.py: list owns listitem directly or through a group (["group", "listitem"]). The WAI-ARIA 1.2 role table names only listitem; WAI-ARIA 1.1 listed the group chain and ACT rule bc4a75 still passes that shape, so the chain is kept.

Property record

For every state and property: name, module, isState, type (boolean, booleanUndefined, tristate, idref, idrefList, integer, number, string, token, tokenList), tokens, default, global, globalExceptRoles (the roles a global attribute is prohibited on), usedInRoles, inheritsIntoRoles and deprecated (the whole attribute).

HTML-AAM mapping

HtmlRoleMapper implements section 3.5 (element role mappings), with every condition the tables state. The text of the tables (heading, WAI-ARIA mapping column, comments) is kept in html-aam-elements.json, and the attribute tables of section 3.6 in html-aam-attributes.json; a test compares every entry that is a plain tag name with the mapper.

ElementRole
a, arealink with href, otherwise generic
imgimg (computed image); with an empty alt after trimming ASCII whitespace, none unless aria-label has text or aria-labelledby names an element
header, footerbanner, contentinfo when no article, aside, main, nav or section ancestor exists; otherwise generic
section, formregion, form when the element has an accessible name; otherwise generic
asidecomplementary when the nearest ancestor among article, aside, nav, section, main, body is main or body; otherwise complementary only with a name, else generic
selectlistbox with multiple or size above 1, otherwise combobox
inputby type: button, image, reset, submit: button; checkbox: checkbox (switch with the switch attribute); radio; number: spinbutton; range: slider; search: searchbox; text, tel, url, email: textbox; text, search, tel, url and email become combobox when list names a datalist of the document; color, date, datetime-local, file, hidden, month, password, time, week: no role. An invalid type is text.
thcolumnheader for scope col or colgroup, rowheader for row or rowgroup; with scope auto, columnheader in a thead or a row without td, rowheader in a row with one
tdgridcell when the nearest table ancestor has an explicit grid or treegrid role, otherwise cell
lilistitem when the nearest ancestor whose role is not generic or presentational has the list role; otherwise generic
optionoption in a select, an optgroup of a select, or a datalist; otherwise no role
summarynone for the first summary child of a details, otherwise generic
othersas the table lists; elements it does not list are generic; "No corresponding role" is no role

"Has an accessible name" is not computed here. RoleOptions.HasAccessibleName supplies the answer; without it the role without a name is reported and NodeRole.RoleWhenNamed holds the role the name would give.

HtmlImplicitProperties.Get(element) returns the implied states and properties of section 3.6 that markup alone determines (required → aria-required, disabled, readonly, checked, selected, multiple, placeholder, list → aria-controls, min/max/value of meter, progress, range and number inputs, colspan, rowspan, open of details on its summary, the heading level, textarea multiline), each with the id of the table entry.

Role computation

RoleComputer.Compute(document, options, cancellationToken) visits every element in document order with an explicit stack. It does not enter template content. For each element:

  1. Explicit role (WAI-ARIA 1.2 section 8.1). Split the role attribute on ASCII whitespace; the first token that names a non-abstract role, ASCII case-insensitively, wins (image counts as img). Tokens before it that are unknown or abstract are reported. region and form tokens are skipped when the element has no accessible name (profile entry unnamed-landmark-token). At most MaxRoleTokens (1024) tokens are examined.
  2. Implicit role from the mapping above.
  3. Static focusability (HTML 6.6.3 without style): a tabindex that parses as an integer, links with href, enabled button, select, textarea and non-hidden input, iframe, the first summary of a details, media with controls, editing hosts; never inside an inert subtree, and never for a control disabled by its own attribute or by a disabled fieldset ancestor (outside that fieldset's first legend).
  4. Resolution.
    • explicit non-presentational role: that role (Explicit);
    • explicit none or presentation: if the element is focusable or has any global aria-* attribute, the implicit role (ConflictResolved); otherwise none (Explicit). Non-global attributes do not prevent it;
    • no explicit role, the element is a required owned element of a presentational parent (the closure of the parent's native role's required owned elements: table owns row, rowgroup, cell, columnheader, gridcell, rowheader; list owns listitem) and is not focusable: none (InheritedPresentational); an element with its own non-presentational explicit role keeps it;
    • otherwise the implicit role (Implicit), or no role (None).
  5. Presentational children. A descendant of an element whose resolved role has presentational children is none (ChildrenPresentational) unless focusable. The element's attribute checks still use its own role.

The result for each element is a NodeRole: Role, ComputedRole (Core-AAM's names: presentation is none, img is image, directory is list; empty when there is no role), ExplicitRole, ImplicitRole, RoleWhenNamed, Origin, IsFocusable, IsDisabled, IsInert and Diagnostics.

Diagnostics

For the element's own role (an inherited presentational role is not used for these checks):

KindWhen
InvalidRoleToken, AbstractRolea token before the chosen one (or every token when none is usable) is unknown or abstract
ProhibitedAttributethe role prohibits the attribute, or the attribute is global with the role in its exception list
UnsupportedAttributea non-global attribute the role does not support
DeprecatedAttributedeprecated on the role, or the whole attribute is
UnknownAttributean aria- name that is not a state or property
InvalidAttributeValuethe value does not fit the type: booleans, tristates and tokens against the token list, token lists token by token, integers (with the lower bounds in the profile), real numbers, an ID reference with more than one token
BrokenIdReferencean ID reference names no element of the document (each token of a list)
MissingRequiredAttributean explicit role that differs from the element's native role lacks a required state or property that the element does not supply natively (profile entry required-attribute-native); requiredWhenFocusable applies to focusable elements
MissingRequiredContext, MissingRequiredOwnedElementsreserved: they depend on the tree with aria-owns re-parenting, which a later step builds. AriaRole.RequiredContext and AriaRole.RequiredOwned hold the data for it.
LimitExceededa limit stopped the scan of role tokens, ID references or diagnostics

An empty attribute value is treated as not specified and is not reported. Diagnostics are in attribute order.

Limits

No recursion follows the depth of the input. Everything is linear in the number of elements, attributes and tokens: MaxRoleTokens (1024), MaxIdReferencesPerAttribute (4096) and MaxDiagnosticsPerElement (64) bound the work and the output per element; the per-row header scan runs once per row. The computation observes the cancellation token every 1024 nodes.

Conformance

testdata/wpt/aria/ holds the role pages of web-platform-tests html-aam/ and wai-aria/role/ at the commit recorded in MANIFEST.md. WptRoleTests translates the AriaUtils.verify* and assignAndVerifyRolesByRoleNames calls and the get_computed_role assertions of those pages, with a name probe of aria-label, a resolvable aria-labelledby, or title. testdata/wpt/aria/expectations.tsv lists each known failure with its reason.

ResultCount
Pass536
Known failure (tentative pages)18
Not applicable5

Known failures are all on tentative pages: sectionheader/sectionfooter for header and footer inside sectioning content or main (the profile exposes generic), the draft "minimum role" rule that gives div elements with autofocus, draggable or popover the group role, and a li that only aria-owns makes a list item. The five not-applicable cases are expectations the pages themselves mark ambiguous (SPEC_AMBIGUOUS_LOG_VALUE). Cases a page builds with script (document.createElement) are not run. The pages html-aam/names.html, hidden-names.html, figure-name-no-figcaption.tentative.html, img-src-srcset-roles.tentative.html and accname-computation-by-element/ test names or need layout and are not included.