HTML ARIA Basics
ARIA stands for Accessible Rich Internet Applications, a set of attributes you can add to HTML elements to give assistive technologies — like screen readers — extra information about what an element is, what state it’s in, and how it relates to other elements. ARIA doesn’t change how an element looks or behaves for sighted mouse users; it only changes what gets exposed to the browser’s accessibility tree, which is what screen readers and other assistive tools actually read. Used correctly, ARIA fills gaps that plain HTML can’t cover. Used incorrectly, it can make a page less accessible than if you’d used no ARIA at all.
Overview / How it works
Every element the browser renders gets a node in two trees: the visual DOM tree you’re used to, and a separate accessibility tree. The accessibility tree stores, for each element, a computed role (what kind of thing it is — a button, a heading, a link, a checkbox), an accessible name (the label a screen reader announces), and a set of states and properties (is it checked, expanded, disabled, required, and so on). Screen readers, browser extensions, and other assistive software query this tree instead of reading raw HTML tags.
Most native HTML elements already have a correct role and behavior built in for free. A <button> is automatically exposed with role button, is keyboard-focusable, and responds to Enter and Space — you get all of that from the browser without writing a single ARIA attribute. This is the foundation of what’s often called the First Rule of ARIA: if a native HTML element or attribute already has the semantics and behavior you need, use it instead of re-purposing a generic element with ARIA. ARIA exists for the gaps: custom widgets that HTML has no native element for (tab panels, tree views, live status messages), or cases where you need to describe a relationship or a dynamic state that plain markup can’t express.
ARIA attributes fall into three broad categories:
- Roles — declared with the
roleattribute, they tell assistive tech what kind of widget an element represents (role="alert",role="tablist",role="navigation"). - Properties — attributes prefixed
aria-that describe characteristics that don’t usually change, such asaria-label(an accessible name) oraria-describedby(a link to descriptive text elsewhere on the page). - States — attributes prefixed
aria-that describe a condition that can change at runtime, such asaria-expanded,aria-checked, oraria-hidden.
Crucially, ARIA is purely semantic — it carries no built-in keyboard behavior and no visual styling. Adding role="button" to a <div> tells a screen reader “this is a button,” but it does not make the div focusable or make it respond to a keypress. You have to add that behavior yourself, which is exactly why reaching for a native element first is almost always less work and more reliable.
Syntax
<element role="role-name" aria-property="value" aria-state="value">...</element>
| Part | Purpose |
|---|---|
role |
Overrides or supplies the element’s exposed role, e.g. role="alert", role="dialog", role="tab". |
aria-label |
Supplies an accessible name directly as a string, used when there is no visible text to serve as the label. |
aria-labelledby |
Points to the id of another element whose text content becomes this element’s accessible name. |
aria-describedby |
Points to the id of an element that provides extended description, announced after the name. |
aria-hidden |
When "true", removes the element (and its children) from the accessibility tree entirely, even though it’s still visible. |
aria-expanded |
State on a control that toggles a section, "true" or "false", for disclosure widgets, menus, and accordions. |
aria-controls |
Points to the id of the element that this control expands, collapses, or otherwise affects. |
aria-live |
Marks a region as “live,” so screen readers announce content changes inside it automatically (polite or assertive). |
Examples
Example 1: Labeling an icon-only button
<button type="button" aria-label="Close dialog">
×
</button>
Result: The browser renders a small button containing a multiplication-sign glyph (×). Visually there is no word “close” anywhere, but a screen reader announces “Close dialog, button” when it receives focus, because aria-label overrides the accessible name that would otherwise be computed from the button’s visible text content.
This is one of the most common legitimate uses of ARIA: the button is already a native, keyboard-accessible <button>, so no role or keyboard behavior needs to be added. aria-label only supplies the missing accessible name for an icon-only control.
Example 2: A polite live region for status updates
<form>
<label for="email">Email</label>
<input type="email" id="email" name="email">
<button type="submit">Subscribe</button>
</form>
<p id="status" role="status" aria-live="polite"></p>
Result: On first render this shows a labeled email field and a Subscribe button; the paragraph below is empty and invisible in normal flow. If a script later inserts text such as “Subscribed successfully” into the #status paragraph, a screen reader announces that new text automatically, without the user needing to move focus to it.
role="status" already implies aria-live="polite" in most browsers, but pairing them explicitly is a common, defensive pattern. “Polite” means the announcement waits for the screen reader to finish whatever it’s currently reading, rather than interrupting immediately (which is what aria-live="assertive" does).
Example 3: A disclosure widget with aria-expanded and aria-controls
<button type="button" aria-expanded="false" aria-controls="faq-answer-1">
What is ARIA?
</button>
<div id="faq-answer-1" hidden>
<p>ARIA is a set of attributes that describe roles, states, and properties for assistive technology.</p>
</div>
Result: A single clickable question is shown; the answer paragraph is not rendered at all because of the hidden attribute. A screen reader announces the button as “What is ARIA?, collapsed, button.” When a script toggles aria-expanded to "true" and removes hidden from the div (typically in response to a click), the answer becomes visible and the button is now announced as “expanded.”
aria-controls tells assistive tech which element the button governs, and aria-expanded reports whether that element is currently open. Note this example describes only the markup and resulting states — wiring the actual toggle behavior is JavaScript, which belongs to a JS course, not this HTML lesson.
How it works step by step
- The HTML parser builds the DOM tree exactly as it would without any ARIA attributes present — ARIA never changes parsing or DOM structure.
- After the DOM is built, the browser computes an accessibility tree in parallel. For each node, it works out a role: native elements get an implicit role (a
<nav>gets rolenavigation, an<h2>gets roleheadingwith level 2); aroleattribute, if present, overrides that implicit role. - The browser then computes the accessible name using a defined priority order:
aria-labelledbywins first, thenaria-label, then native labeling mechanisms (a<label>element, alt text, the element’s own text content), roughly in that order depending on the element type. - States and properties (
aria-expanded,aria-checked,aria-hidden, and so on) are attached to the node as additional metadata. - The operating system exposes this accessibility tree through a platform API (like UI Automation on Windows or NSAccessibility on macOS), which is what screen readers actually query — they never read your raw HTML tags directly.
- If script later changes an
aria-*attribute’s value, the browser updates the accessibility tree node, and if that node sits inside a live region, the screen reader is notified of the change immediately.
Common Mistakes
Mistake 1: Building a fake button out of a div
<div role="button" onclick="submitForm()">
Submit
</div>
This tells a screen reader the div is a button, but the div still isn’t in the natural tab order and doesn’t respond to Enter or Space. A keyboard-only user can hear “Submit, button” but has no way to activate it. Adding role="button" promises behavior that the element doesn’t actually have.
<button type="button" onclick="submitForm()">
Submit
</button>
The corrected version uses the native <button> element, which is focusable, keyboard-operable, and correctly exposed to assistive tech automatically — with no ARIA needed at all.
Mistake 2: Hiding a still-focusable interactive element
<a href="/cart" aria-hidden="true">
View cart
</a>
aria-hidden="true" removes the link from the accessibility tree, so a screen reader user never hears “View cart” and doesn’t know it exists — but the link is still visible and still receives keyboard focus when tabbing. The result is a “ghost” stop in the tab order: a keyboard user tabs onto something, the screen reader announces nothing at all, and they have no idea what they just focused.
<a href="/cart">
View cart
</a>
Unless an element is genuinely decorative and also removed from the tab order (or is a duplicate of content announced elsewhere), don’t apply aria-hidden to anything a keyboard user can still focus.
Best Practices
- Reach for a native HTML element or attribute first; only add ARIA when there’s genuinely no native equivalent for what you’re building.
- Never change an element’s native, correct semantics with a conflicting role — for example, don’t put
role="heading"on an element that isn’t structurally a heading, orrole="link"on a<button>. - Every interactive role you add with ARIA (like
role="button"on a non-button element) must also get real keyboard support and atabindex, or it will be unusable without a mouse. - Use
aria-labelonly when there’s no visible text available to serve as a name; if visible text exists, preferaria-labelledbypointing at it, or just rely on the text itself. - Don’t apply
aria-hidden="true"to any element that can still receive keyboard focus. - Test with an actual screen reader (VoiceOver, NVDA, or JAWS) periodically — ARIA attributes that look correct in markup can still produce a confusing or silent experience in practice.
- Prefer
aria-live="polite"for most status updates; reservearia-live="assertive"for urgent, rare interruptions like error messages, since assertive announcements cut off whatever the screen reader was already saying.
Practice Exercises
- Write a search form containing a text
<input>with no visible<label>, and give it a proper accessible name using only an ARIA attribute (no placeholder-as-label trick). - Build a collapsible “Read more” section using a
<button>and a<div>, correctly wired witharia-expandedandaria-controlsin their initial (collapsed) state. - Take a
<div role="checkbox">custom widget and list, in prose, every native HTML attribute and behavior a real<input type="checkbox">would have given you for free, that the div now has to reproduce manually.
Summary
- ARIA attributes add roles, states, and properties to the browser’s accessibility tree, which is what screen readers actually read.
- ARIA changes only what’s exposed to assistive technology — never visual appearance, and never keyboard behavior on its own.
- Prefer native HTML elements first; use ARIA to fill genuine gaps, like custom widgets or dynamic live regions.
aria-labelandaria-labelledbysupply accessible names;aria-expanded,aria-checked, andaria-hiddendescribe states.- Any custom role you add must come with matching keyboard support, or you’ve made the page worse, not better.
- Never hide a focusable element with
aria-hidden="true"— it creates a silent, unexplained stop in the tab order.
