ARIA Basics: Roles, States and Properties
ARIA fills gaps that HTML cannot. It also breaks pages when used carelessly. The rules, the attributes worth knowing, and the ones to leave alone.
- Concept
- The five rules
- The three kinds of attribute
- Roles - what a thing is
- States - the current condition, and they change
- Properties - relationships, largely static
- The handful that come up constantly
- aria-expanded
- aria-current
- aria-live
- aria-describedby
- role="presentation"
- Where ARIA does earn its place
- A tab interface, since HTML has none
- Bad ARIA is worse than none
- Important rules
- Common mistakes
- Best practices
- Practice
-
HTML Basics
- What is HTML: The Structure Layer of Every Web Page
- HTML Document Structure: DOCTYPE, html, head and body
- Elements, Tags and Attributes: The Vocabulary of HTML
- HTML Comments: Notes That Ship With Your Code
- Block Level and Inline Elements
- Writing and Running Your First HTML Page
- How a Browser Turns Markup Into a Page
- Text and Formatting
- Links and Navigation
- Images and Media
- Lists
- Tables
-
Forms
- Form Structure: form, action and method
- Input Types: Text, Email, Number, Date and the Rest
- Labels: The Most Important Element in a Form
- Checkboxes, Radio Buttons and Grouping
- select, option, optgroup and datalist
- textarea, File Uploads and Hidden Fields
- Buttons: submit, reset and button
- Built In Form Validation
- GET or POST: What Happens When a Form Is Submitted
- Semantic HTML
- HTML5 Features
- Head and Metadata
- HTML with CSS
- HTML with JavaScript
- Accessibility
-
HTML SEO
- How Google Works: Crawling, Indexing and Ranking
- SEO Friendly HTML Structure
- Titles and Descriptions That Earn Clicks
- Headings and Content Structure for Search
- Internal Linking and Anchor Text
- robots.txt and XML Sitemaps
- Canonical URLs and Duplicate Content
- Structured Data and JSON-LD
- Image SEO
- Core Web Vitals and Mobile Friendliness
- DevTools and Debugging
- Editor Productivity
- HTML Best Practices
- HTML Projects
- Advanced Projects
- Practice and Exams
Concept
ARIA - Accessible Rich Internet Applications - is a set of attributes that describe roles, states and relationships to assistive technology when HTML alone cannot.
It has one property worth understanding above all others: ARIA changes what is announced. It never changes behaviour. Adding role="button" to a div makes a screen reader say button. It does not make the element focusable, does not make Enter or Space work, and does not give it a disabled state.
The five rules
- Do not use ARIA if a native element exists.
buttonbeatsdiv role="button"every time. - Do not change native semantics unless you must.
<h2 role="tab">removes the heading from the outline. - Every interactive ARIA control must be keyboard operable. Which you have to implement yourself.
- Do not put
role="presentation"oraria-hidden="true"on a focusable element. It creates a control that can be reached but not described. - Every interactive element needs an accessible name.
The three kinds of attribute
Roles - what a thing is
role="button" role="tab" role="tabpanel"
role="dialog" role="alert" role="status"
role="navigation" role="search" role="region"
role="tooltip" role="menu" role="progressbar"
role="presentation"States - the current condition, and they change
aria-expanded="true" aria-checked="false"
aria-selected="true" aria-disabled="true"
aria-hidden="true" aria-invalid="true"
aria-busy="true" aria-pressed="true"
aria-current="page"Properties - relationships, largely static
aria-label="Close" aria-labelledby="heading-id"
aria-describedby="hint-id" aria-controls="panel-id"
aria-live="polite" aria-required="true"
aria-haspopup="true"The handful that come up constantly
aria-expanded
<button type="button" aria-expanded="false" aria-controls="menu">Menu</button>
<ul id="menu" hidden>...</ul>button.addEventListener("click", () => {
const open = button.getAttribute("aria-expanded") === "true";
button.setAttribute("aria-expanded", String(!open));
menu.hidden = open;
});The most commonly needed ARIA attribute, and the most commonly left stale. If the script toggles a class but not the attribute, the reader is told the menu is closed while it is open.
aria-current
<a href="/courses" aria-current="page">Courses</a>
<li aria-current="step">Step 2</li>aria-live
Announces content that changes without the reader doing anything - a search result count, a save confirmation, a validation summary.
<!-- announced at the next natural pause -->
<p id="status" aria-live="polite"></p>
<!-- interrupts immediately: errors only -->
<p id="error" aria-live="assertive"></p>
<!-- shorthands with built in roles -->
<p role="status"></p> <!-- equivalent to polite -->
<p role="alert"></p> <!-- equivalent to assertive -->The region must be in the DOM before the content changes. Inserting an element that already contains the message announces nothing - the browser only watches regions it already knows about.
// nothing is announced
document.body.insertAdjacentHTML("beforeend", "<p aria-live='polite'>Saved</p>");
// announced
document.getElementById("status").textContent = "Saved";aria-describedby
<input id="pin" aria-describedby="pin-hint">
<small id="pin-hint">Six digits</small>role="presentation"
<!-- strips table semantics from a legacy layout table -->
<table role="presentation">...</table>Only correct for a layout table you cannot rewrite. The real fix is CSS grid.
Where ARIA does earn its place
- Tabs, since HTML has no tab element.
- Combo boxes with filtering.
- Tree views.
- Live regions for dynamic updates.
- Labelling landmarks:
<nav aria-label="Breadcrumb">. - Marking the current page or step.
- Describing a control with hint text.
Everything else - buttons, links, headings, lists, forms, dialogs, disclosure widgets - has a native element that already does the job.
A tab interface, since HTML has none
<div role="tablist" aria-label="Course information">
<button role="tab" id="tab-overview" aria-selected="true"
aria-controls="panel-overview" tabindex="0">Overview</button>
<button role="tab" id="tab-fees" aria-selected="false"
aria-controls="panel-fees" tabindex="-1">Fees</button>
</div>
<div role="tabpanel" id="panel-overview" aria-labelledby="tab-overview" tabindex="0">
...
</div>
<div role="tabpanel" id="panel-fees" aria-labelledby="tab-fees" tabindex="0" hidden>
...
</div>Note the tabindex pattern: the selected tab is 0 and the others are -1, so the whole tablist is one tab stop and arrow keys move between tabs. That arrow key handling has to be written in JavaScript - the markup alone does nothing.
This is what ARIA is a contract means. Declaring role="tablist" promises tab behaviour, and a screen reader user will expect it. If the behaviour is not there, the ARIA has made things worse.
Bad ARIA is worse than none
Surveys of large numbers of home pages consistently find that pages using ARIA have more detected accessibility errors on average than pages using none - because ARIA is usually added to complex interfaces, and usually added incompletely.
<!-- announced as a button, and still unusable -->
<div role="button">Save</div>
<!-- the label is a lie: voice control users say what they see -->
<button aria-label="Submit">Send enquiry</button>
<!-- reachable by keyboard, invisible to a screen reader -->
<a href="/x" aria-hidden="true">Link</a>
<!-- the heading has left the outline -->
<h2 role="presentation">Fees</h2>Important rules
- ARIA changes announcements, never behaviour.
- Every ARIA widget role brings a keyboard contract you must implement.
- Referenced ids must exist.
- State attributes must be kept accurate.
- Live regions must exist before their content changes.
- Do not restate implicit roles.
Common mistakes
- Adding roles to
divelements instead of using native ones. - Stale
aria-expandedvalues. aria-labelthat disagrees with the visible text.- Live regions inserted along with their message.
- Broken id references.
aria-hiddenon focusable elements.- Declaring a widget role without the keyboard behaviour it implies.
Best practices
- Native element first, every time.
- Learn five attributes well -
aria-expanded,aria-current,aria-live,aria-describedby,aria-label- rather than all of them badly. - Update state in the same function that updates the interface.
- Follow the published authoring practices pattern for any widget role.
- Test with a screen reader; ARIA cannot be verified by looking.
- When unsure, leave it out. No ARIA beats wrong ARIA.
Practice
- Build a disclosure button with correct
aria-expandedtoggling, then break the toggle and listen to the difference. - Add a polite live region for a result count and confirm it is announced on change.
- Insert a live region together with its message and explain why nothing is announced.
- Take a
div role="button"and list everything missing compared with a real button.