Package Exports
- html-aria
- html-aria/get-required-attributes
- html-aria/get-role
- html-aria/get-supported-attributes
- html-aria/get-supported-roles
- html-aria/index
- html-aria/lib/aria-attributes
- html-aria/lib/aria-roles
- html-aria/lib/html
- html-aria/lib/util
- html-aria/package.json
- html-aria/tags/footer
- html-aria/tags/header
- html-aria/tags/input
- html-aria/tags/select
- html-aria/tags/td
- html-aria/tags/th
Readme
html-aria
WAI-ARIA utilities for HTML based on the ARIA 1.3 spec and latest HTML in ARIA recommendations (Dec 2024). Lightweight (5k gzip), performant, and zero dependencies.
⚠️ This is in beta and subject to change.
VS other libraries
aria-query
- aria-query is still on ARIA 1.2; this library supports ARIA 1.3
- html-aria is > 100× faster, due to aria-query rebuilding large arrays on almost every query.
- html-aria is smaller, weighing only ~5k gzip (aria-query is ~13k gzip)
- html-aria is more user-friendly, with APIs like
getRole()rather than having to write boilerplate code - html-aria respects more nuance in the spec such as improved role detection from HTML and HTML-aware aria-* attributes
Setup
npm i html-ariaAPI
getRole()
Get a valid ARIA role from HTML. This is the core API.
import { getRole } from "html-aria";
getRole(document.createElement("article")); // "article"
getRole({ tagName: "input", attributes: { type: "checkbox" } }); // "checkbox"
getRole({ tagName: "div", attributes: { role: "button" } }); // "button"In order to describe how this library follows the W3C spec more closely, it’s important to understand that inferring ARIA roles from HTML isn’t simple! There are essentially 3 categories of roles:
- Simple roles: 1 HTML element = 1 default ARIA role.
- Attribute roles: The proper ARIA role can only be determined from the HTML element’s attributes (e.g.
input[type="radio"]→radio) - Hierarchial roles: The proper ARIA role can only be determined by knowing its parent roles.
The 1st type only requres a simple tag name. The 2nd requires a tag name and attributes. The 3rd requires knowing its immediate parent elements. To see a table of all types, see 3 types of roles.
getSupportedRoles()
The spec dictates that certain elements may NOT receive certain roles. For example, <div role="button"> is allowed (not recommended, but allowed), but <select role="button"> is not. getSupportedRoles() will return all valid roles for a given element (including attributes).
import { getSupportedRoles } from "html-aria";
getSupportedRoles(document.createElement("img")); // ["none", "presentation", "img"]
getSupportedRoles({ tagName: "img", attributes: { alt: "Image caption" } }); // ["button", "checkbox", "link", (15 more)]getSupportedAttributes()
For any element, list all supported aria-* attributes, including attributes inherited from superclasses. This takes in an HTML element, not an ARIA role, because in some cases the HTML element actually affects the list (see full list).
import { getSupportedAttributes } from "html-aria";
getSupportedAttributes({ tagName: "button" }); // ["aria-atomic", "aria-braillelabel", …]If you want to look up by ARIA role instead, just pass in a placeholder element:
getSupportedAttributes({ tagName: "div", attributes: { role: "combobox" } });There’s also a helper method isSupportedAttribute() to test individual attributes:
import { isSupportedAttribute } from "html-aria";
isSupportedAttribute({ tagName: "button" }, "aria-pressed"); // true
isSupportedAttribute({ tagName: "button" }, "aria-checked"); // falseIt’s worth noting that HTML elements may factor in according to the spec. In other words, just providing the role isn’t enough. Here are a list of HTML elements where they support different attributes than their corresponding roles:
<b>,<code>,<i>,<samp>,<span>, and<small>don’t allowaria-labeloraria-labelledby<col>,<colgroup>,<slot>,<source>, and<template>don’t support any aria-* attributes (whereas by default global attributes are usually allowed)
isValidAttributeValue()
Some aria-* attributes require specific values. isValidAttributeValue() returns false if, given a specific aria-* attribute, the value is invalid according to the spec.
import { isValidAttributeValue } from "html-aria";
// string attributes
// Note: string attributes will always return `true` except for an empty string
isValidAttributeValue("aria-label", "This is a label"); // true
isValidAttributeValue("aria-label", ""); // false
// boolean attributes
isValidAttributeValue("aria-disabled", true); // true
isValidAttributeValue("aria-disabled", false); // true
isValidAttributeValue("aria-disabled", "true"); // true
isValidAttributeValue("aria-disabled", 1); // false
isValidAttributeValue("aria-disabled", "disabled"); // false
// enum attributes
isValidAttributeValue("aria-checked", "true"); // true
isValidAttributeValue("aria-checked", "mixed"); // true
isValidAttributeValue("aria-checked", "checked"); // false⚠️ Be mindful of cases where a valid value may still be valid, but invoke different behavior according to the ARIA role, e.g. mixed behavior for radio/menuitemradio/switch
Reference
ARIA roles from HTML
This outlines the requirements to adhere to the W3C spec when it comes to inferring the correct ARIA roles from HTML. There are 3 basic types:
- Simple roles: 1 HTML element = 1 default ARIA role
- Attributes roles: 1 HTML element = multiple possible ARIA roles, depending on attributes (
input[type="radio"]→radiois a common example) - Hierarchial roles: 1 HTML element = multiple possible ARIA roles depending on its parents
Here are all the HTML elements where either attributes, hierarchy, or both are necessary to determine the correct role (all omitted elements follow a simple mapping pattern):
| Element | Role | Attribute-based | Hierarchy-based |
|---|---|---|---|
| a | generic | link |
✅ | |
| area | generic | link |
✅ | |
| footer | contentinfo | generic |
✅ | |
| header | banner | generic |
✅ | |
| input | button | checkbox | combobox | radio | searchbox | slider | spinbutton | textbox |
✅ | |
| li | listitem | generic |
✅ | |
| section | generic | region |
✅ | |
| select | combobox | listbox |
✅ | |
| td | cell| gridcell | — |
✅ | |
| th | columnheader | rowheader | — |
✅ | ✅ |
Note: — = no corresponding role
aria-* attributes from HTML
Further, a common mistake many simple accessibility libraries make is mapping aria-* attributes to ARIA roles. While that mostly works, there are a few exceptions where HTML information is needed. That is why getSupportedAttributes() takes an HTML element. Here is a full list:
| Element | Default Role | Notes |
|---|---|---|
| base | generic |
No aria-* attributes allowed |
| body | generic |
Does NOT allow aria-hidden="true" |
| br | generic |
No aria-* attributes allowed EXCEPT aria-hidden |
| col | — | No aria-* attributes allowed |
| colgroup | — | No aria-* attributes allowed |
| datalist | listbox |
No aria-* attributes allowed |
| head | — | No aria-* attributes allowed |
| html | — | No aria-* attributes allowed |
img (no alt) |
none |
No aria-* attributes allowed EXCEPT aria-hidden |
| input[type=hidden] | — | No aria-* attributes allowed |
| link | — | No aria-* attributes allowed |
| map | — | No aria-* attributes allowed |
| meta | — | No aria-* attributes allowed |
| noscript | — | No aria-* attributes allowed |
| picture | — | No aria-* attributes allowed EXCEPT aria-hidden |
| script | — | No aria-* attributes allowed |
| slot | — | No aria-* attributes allowed |
| source | — | No aria-* attributes allowed |
| style | — | No aria-* attributes allowed |
| template | — | No aria-* attributes allowed |
| title | — | No aria-* attributes allowed |
| track | — | No aria-* attributes allowed EXCEPT aria-hidden |
| wbr | — | No aria-* attributes allowed EXCEPT aria-hidden |
Note: — = no corresponding role. Also worth pointing out that in other cases, global aria-* attributes are allowed, so this is unique to the element and NOT the ARIA role.
Technical deviations from the spec
SVG
SVG is tricky. Though the spec says <svg> should get the graphics-document role by default, browsers chose chaos. Firefox 134 displays graphics-document, Chrome 131 defaults to image (previously it returned nothing, or other roles), and Safari defaults to generic (which is one of the worst roles you could probably give it).
Since we have 1 spec and 1 browser agreeing, this library defaults to graphics-document. Though the best answer is SVGs should ALWAYS get an explicit role.
Ancestor-based roles
In regards to ARIA roles in HTML, the spec gives non-semantic roles to <td>, <th>, and <li> UNLESS they are used inside specific containers (table, grid, or gridcell for <td>/<th>; list or menu for <li>). This library assumes they’re being used in their proper containers without requiring the ancestors array. This is done to avoid the footgun of requiring missable configuration to produce accurate results, which is bad software design.
Instead, the non-semantic roles must be “opted in” by passing an explicitly-empty ancestors array:
import { getRole } from "html-aria";
getRole({ tagName: "td" }, { ancestors: [] }); // undefined
getRole({ tagName: "th" }, { ancestors: [] }); // undefined
getRole({ tagName: "li" }, { ancestors: [] }); // "generic"FAQ
Why the { tagName: string } object syntax?
Most of the time this library will be used in a Node.js environment, likely outside the DOM (e.g. an ESLint plugin traversing an AST). While most methods also allow an HTMLElement as input, the object syntax is universal and works in any context.
What’s the difference between “no corresponding role” and the none role?
From the spec:
No corresponding role
The elements marked with No corresponding role, in the second column of the table do not have any implicit ARIA semantics, but they do have meaning and this meaning may be represented in roles, states and properties not provided by ARIA, and exposed to users of assistive technology via accessibility APIs. It is therefore recommended that authors add a
roleattribute to a semantically neutral element such as adivor span, rather than overriding the semantics of the listed elements.
none role
An element whose implicit native role semantics will not be mapped to the accessibility API. See synonym presentation.
In other words, none is more of a decisive “this element is presentational and can be ignored” labeling, while “no corresponding role” means “this element doesn’t have predefined behavior that can be automatically determined, and the author should provide additional information such as explicit roles and ARIA states and properties.”
In this library, “no corresponding role” is represented as undefined.
About
Project Goals
- Deliver correct, up-to-date information based on the latest W3C recommendation
- Only provide normative (unopinionated) data
Differences from aria-query
- Current with ARIA 1.3
- Simpler API.
- Ships TypeScript types for advanced usecases