Section 3 · Keyboard, Focus and ARIA
ARIA: Roles, States and Live Regions
18 min3 min readCode playground
Lesson at a glance
Apply ARIA only where HTML falls short, keep states such as aria-pressed and aria-expanded in sync, and announce dynamic updates with status and alert live regions.
No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.
Everything so far has used plain HTML, and that is deliberate. HTML elements come with roles, names, states and keyboard behaviour built in. But sometimes HTML runs out. There is no native element for a toggle button’s pressed state, for a set of tabs, or for “say this out loud when the basket changes”. That is where ARIA (Accessible Rich Internet Applications) comes in: a set of attributes that let you describe roles, states and relationships directly to the accessibility tree.
The first rule of ARIA
The W3C’s own guidance starts with a rule that surprises people: if you can use a native HTML element with the behaviour you need, do that instead of ARIA. WebAIM’s annual scans of the top million home pages keep finding that pages using ARIA have more detected errors, not fewer. The reason is simple. ARIA only changes what is announced. It doesn’t make anything focusable, doesn’t add keyboard handling, and doesn’t change appearance. Every ARIA attribute is a promise to assistive technology that your JavaScript then has to keep.
<!-- A promise with nothing behind it -->
<span role="button">Add to basket</span>
<!-- The same promise, kept by the browser -->
<button type="button">Add to basket</button>Roles, properties and states
ARIA attributes fall into three groups.
Roles say what something is: role="tab", role="dialog", role="switch", role="status". You rarely need them for things HTML already has.
Properties describe relationships or fixed characteristics: aria-labelledby and aria-describedby (you used both on forms), aria-controls, aria-required, aria-haspopup.
States describe things that change as the user interacts, and your script must update them: aria-expanded, aria-pressed, aria-checked, aria-selected, aria-current, aria-disabled, aria-invalid.
Ticketly’s wishlist heart is a good example. It is already a real button, but nothing tells a screen reader whether the event is saved. aria-pressed turns it into a toggle button:
<button type="button" class="fav" aria-pressed="false">Save to wishlist</button>fav.addEventListener('click', () => {
const saved = fav.getAttribute('aria-pressed') === 'true';
fav.setAttribute('aria-pressed', String(!saved));
});NVDA now says “Save to wishlist, toggle button, not pressed”, then “pressed” after activation. Notice that the label stays the same; the state carries the change. And you can style from the state directly, which keeps the visuals and the semantics from drifting apart:
.fav[aria-pressed='true'] {
background: #5b3cc4;
color: #fff;
}For the current page in a navigation menu, aria-current="page" tells screen reader users which link they are on: “Tickets, current page, link”.
A few more rules from the W3C’s “Using ARIA” note keep you out of trouble:
- Don’t change native semantics unless you really must:
<h2 role="button">destroys the heading. Put a button inside the heading instead. - Every interactive ARIA control must be keyboard operable.
- Every interactive element needs an accessible name.
Live regions: announcing changes
When Ticketly adds a ticket, the basket counter in the header changes from 1 to 2. Sighted users notice. Screen reader users hear nothing, because their focus is still on the button they pressed. A live region is an element whose content changes are read out automatically, without moving focus.
<p id="basket-status" role="status"></p>function addToBasket() {
count += 1;
basketStatus.textContent = `Ticket added. ${count} tickets in your basket.`;
}Three rules make live regions reliable:
- The region must exist before the change. Screen readers watch existing live regions for changes. Inserting a new element that already contains text is often missed. Render the empty region at page load.
- Pick the politeness.
role="status"(same asaria-live="polite") waits until the screen reader finishes its current sentence.role="alert"(assertive) interrupts. Reserve alerts for urgent, rare messages like a failed payment; an assertive “Ticket added” on every click is exhausting. - Keep messages short and meaningful. Announce the result (“2 tickets in your basket”), not the mechanics (“basket component updated”).
ARIA’s states and roles become most powerful in custom widgets, where no HTML element exists at all. The next lesson builds three of them following the ARIA Authoring Practices Guide.
Key takeaways
- The first rule of ARIA is to use a native HTML element when one exists; ARIA changes what is announced, never how anything behaves.
- ARIA comes in roles (what it is), properties (relationships and fixed traits) and states (values that change, which your script must keep in sync).
- Never put aria-hidden=“true” on a focusable element or override the role of a native element that already has the right one.
- A live region must already be in the DOM before its text changes; use role=“status” for polite updates and role=“alert” only for urgent ones.