RizTech Academy logo
RizTech Academy
AccessibilityLesson 5 of 625 min

ARIA: the little you need and the lot you do not

ARIA is a set of attributes that tell assistive technology what an element is and what state it is in. It is genuinely necessary sometimes, and most of the ARIA in the world is doing harm.

So this lesson is mostly about not using it, which is the opposite of how the subject is usually taught.

The first rule

If you can use a native HTML element or attribute instead of ARIA, do that.

That is the actual first rule of the specification's own authoring practices, and it is the whole lesson in one line.

<div role="button" tabindex="0" aria-pressed="false">Save</div>
<button type="button" aria-pressed="false">Save</button>

The second is shorter, responds to Enter and Space, is disableable, is in the buttons list, and cannot be got wrong. The first needs two keydown handlers and still behaves differently.

ARIA changes what assistive technology is told. It changes nothing else. It does not add keyboard support, does not add focusability, does not add click handling, and does not change how anything looks. role="button" on a <div> produces something announced as a button that a keyboard user cannot activate — which is worse than an unlabelled div, because now it also lies.

The five rules

The specification states five, and they are short enough to learn:

1. Use native HTML if you can. As above.

2. Do not change native semantics unless you really must.

<h2 role="tab">Delivery</h2>                    <!-- the heading is now gone -->
<div role="tab"><h2>Delivery</h2></div>         <!-- keep both -->

Putting a role on an element destroys its own role. That h2 is no longer in the headings list.

3. All interactive ARIA controls must be usable by keyboard. If you build it, you implement every expected key — arrows, Home, End, Escape — as the authoring practices specify for that widget.

4. Do not use role="presentation" or aria-hidden="true" on a focusable element. You would create something a keyboard user can reach and a screen reader will not announce. Focused, and silent.

5. Every interactive element needs an accessible name. A button, a link, a form field. Ideally from its visible text.

The ARIA worth actually using

A short list. Almost everything you need is here.

aria-label and aria-labelledby

For naming something with no visible text:

<button type="button" aria-label="Close">
  <svg aria-hidden="true" focusable="false">…</svg>
</button>

<nav aria-label="Breadcrumb">…</nav>

aria-labelledby is better when visible text exists, because it cannot drift out of sync and it is translated with the page:

<section aria-labelledby="delivery-h">
  <h2 id="delivery-h">Delivery</h2>
</section>

Two things go wrong with aria-label:

It replaces the element's text entirely. <button aria-label="Submit">Send enquiry</button> is announced "Submit" — so a voice-control user saying "click send enquiry" gets nothing, because the accessible name no longer matches what they can see. WCAG 2.5.3 requires the visible text to be part of the accessible name. If there is visible text, do not override it.

It does not work on every element. aria-label is ignored on generic elements like a plain <div> or <span> with no role. People add it, see nothing announced, and conclude ARIA is broken.

aria-describedby

Attaches supplementary text — a hint, or an error:

<label for="pin">Pincode</label>
<input id="pin" inputmode="numeric" aria-describedby="pin-hint pin-error"
       aria-invalid="true">
<p id="pin-hint">Six digits, not starting with zero.</p>
<p id="pin-error">That pincode is outside our delivery area.</p>

Read after the label, and it takes a space-separated list of ids. This is the correct way to associate a hint or an error with a field, and it is the one piece of ARIA most forms genuinely need.

aria-expanded, aria-current, aria-pressed

State, on a control that already works:

<button type="button" aria-expanded="false" aria-controls="menu">Menu</button>
<div id="menu" hidden>…</div>

<a href="/prices/" aria-current="page">Prices</a>

<button type="button" aria-pressed="true">Bold</button>

aria-expanded is the single most useful ARIA attribute after labelling — it tells a listener whether the thing is open. aria-current="page" marks the current item in a navigation, which is otherwise conveyed by colour alone and therefore fails the previous lesson.

Note that <details>/<summary> from module 1 handles the expanded state natively, so it needs none of this.

aria-hidden="true"

Hide decoration from assistive technology:

<span aria-hidden="true">⚠</span> Enter a valid number
<svg aria-hidden="true" focusable="false">…</svg>

For an inline SVG icon, aria-hidden="true" and focusable="false" — the second because older Internet Explorer made SVGs focusable, and it is still the habit in every icon library.

Never on anything focusable or containing anything focusable. Rule 4.

aria-live

For content that changes without a page load — the one case where you genuinely cannot do it in plain HTML:

<p id="status" role="status" aria-live="polite"></p>

polite waits for a pause; assertive interrupts and should be rare. role="status" implies aria-live="polite" so the pair is belt and braces.

The critical detail: the region must exist in the DOM, empty, before you put anything in it. If you create the element and its text in one go, many screen readers announce nothing, because they were not watching an element that did not exist. This is the single most common reason a live region "does not work".

role="alert"

An assertive live region for something urgent. Use sparingly — it interrupts whatever the user was listening to.

What not to do

Do not add roles that match the element.

<nav role="navigation">      <!-- redundant -->
<button role="button">       <!-- redundant -->
<ul role="list">             <!-- redundant, usually -->

Harmless but noise. One genuine exception: list-style: none on a <ul> causes Safari to drop the list semantics, so role="list" restores them. That is a real bug workaround, not a general rule.

Do not use ARIA to paper over bad markup.

<div role="heading" aria-level="2">Prices</div>
<h2>Prices</h2>

Both are announced as a level-2 heading. The second cannot be got wrong, is styleable, and appears in the outline without you thinking about it.

Do not build widgets you could avoid. A custom select, tab set, accordion or modal is hundreds of lines of keyboard handling and ARIA state, and there is a native or nearly-native answer to most of them — <select>, <details>, <dialog>, <datalist>. Module 1 covered these deliberately.

If you genuinely must build one, follow the WAI-ARIA Authoring Practices pattern for it exactly. It specifies every role, every state and every key. Improvising is how you get a tab set that announces as a tab set and cannot be operated.

Broken ARIA is worse than none

This is the point of the whole lesson.

With no ARIA, a <div> is announced as nothing in particular and a user knows to be careful. With wrong ARIA, it is announced confidently as a button, or as expanded when it is collapsed, and the user acts on that. You have replaced an absence of information with misinformation.

The common versions: aria-expanded never updated when the menu opens. aria-label disagreeing with the visible text. role="tablist" with no keyboard handling. aria-hidden on a focusable element. And aria-labelledby pointing at an id that does not exist, which is subtler than it sounds: the reference contributes nothing, so the name falls through to the element's own text. A button reading "Save" with aria-labelledby="missing-id" is still announced "Save", so the bug is invisible until the day you put an icon in a button and get silence.

Every automated tool flags invalid ARIA, so the mechanical errors are catchable cheaply. Whether aria-expanded is accurate is not.

Checking what you actually produced

The accessible name and role of an element are computed from several sources, and the only reliable way to know the result is to look.

In devtools: select the element, open Accessibility, and read the computed Name and Role. If the name is empty, or is not what somebody would say out loud when pointing at it, that is your bug.

Do this for every button, link and form field on a page once. It takes ten minutes and finds things you would not guess.

Check your work

The first rule. Use native HTML instead of ARIA wherever you can.

What ARIA changes. Only what assistive technology is told — not behaviour, keyboard support or appearance.

What a role does to an element's own semantics. Destroys them.

Why aria-label on a button with text is a problem. It replaces the name, so voice control no longer matches the visible text.

Where aria-label is ignored. On generic elements with no role.

Why aria-labelledby is preferred. It reuses visible text, so it cannot drift and it is translated.

What a broken aria-labelledby does. Contributes nothing, so the name falls through to the element's own text — which hides the bug until there is no text to fall back to.

What most forms genuinely need. aria-describedby, plus aria-invalid.

The most useful state attribute. aria-expanded.

What aria-current="page" fixes. A current nav item conveyed by colour alone.

The two attributes for a decorative inline SVG. aria-hidden="true" and focusable="false".

The commonest reason a live region does nothing. It did not exist, empty, before the content was added.

Why role="list" is occasionally legitimate. list-style: none drops list semantics in Safari.

Why broken ARIA is worse than none. It replaces missing information with confident misinformation.

How to know what you actually produced. The computed Name and Role in the devtools Accessibility pane.

Practice

  1. Build a div-button with role="button" and tabindex="0". Try Enter and Space. Then replace it with a <button>.
  2. Put role="tab" on an <h2> and check the headings list.
  3. Add aria-label="Submit" to a button whose text says "Send enquiry". Read the computed Name in devtools and explain the voice-control problem.
  4. Put aria-label on a plain <div> and check whether anything is announced.
  5. Replace an aria-label with aria-labelledby pointing at a visible heading.
  6. Point aria-labelledby at an id that does not exist on a button with text, and read the computed Name. Then remove the text and read it again.
  7. Add aria-describedby and aria-invalid to a form field and listen to it.
  8. Add aria-expanded to a menu button, then deliberately never update it. Listen to it open.
  9. Add aria-current="page" to a navigation item and listen to the list.
  10. Put aria-hidden="true" on a <button>, then tab to it.
  11. Build a live region by creating the element and its text together. Then create the empty region first and add text afterwards. Compare.
  12. Add role="navigation" to a <nav> and listen for the duplication.
  13. Apply list-style: none to a <ul> and check the list semantics in Safari, then add role="list".
  14. Open the WAI-ARIA Authoring Practices page for a tab set and count the keyboard interactions required.
  15. Read the computed Name and Role for every interactive element on one of your pages. Write down any that are empty or wrong.

Official documentation

Next: testing all of it, hands on.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship