Browsers expose an accessibility tree to assistive technology. Semantic HTML fills that tree correctly for free. ARIA can patch gaps, but misuse makes things worse than no ARIA at all.
Learning objectives
- Use landmarks, headings and native controls correctly.
- Apply the five rules of ARIA.
- Manage focus for dialogs and dynamic content.
- Build a keyboard-accessible modal.
1. Native first
<!-- wrong: invisible to assistive tech, unusable by keyboard -->
<div class="btn" onclick="save()">Save</div>
<!-- right -->
<button type="button" onclick="save()">Save</button>
A <button> is focusable, activates with Enter and Space, has the button role and gets the right cursor and disabled behaviour. Reproducing that with a div requires tabindex, role, key handlers and still misses details.
Native elements to prefer:
| Need | Element |
|---|---|
| Action | <button> |
| Navigation | <a href> |
| Page structure | <header> <nav> <main> <aside> <footer> |
| Section with a heading | <section aria-labelledby="..."> |
| Form control | <input> <select> <textarea> with <label> |
| Toggle | <input type="checkbox"> or <button aria-pressed> |
| Dialog | <dialog> or role="dialog" + focus management |
2. Headings and landmarks
<h1>Full Stack Development</h1>
<h2>Curriculum</h2>
<h3>Stage 1</h3>
<h3>Stage 2</h3>
<h2>Fees</h2>
One <h1> per page, no skipped levels. Screen-reader users navigate by headings; a page styled with <div class="h2"> has no outline at all.
Landmarks: one <main>, a <nav> per navigation block (label them when there are several), <header> and <footer> at the page level.
<nav aria-label="Primary">
<nav aria-label="Breadcrumb">
3. The five rules of ARIA
- Do not use ARIA if a native element exists.
- Do not change native semantics —
<button role="heading">is a lie. - All interactive ARIA controls must be keyboard operable.
- Never hide a focusable element with
aria-hidden="true"; it stays in the tab order but disappears from the tree. - Every interactive element needs an accessible name.
<button type="button" aria-label="Close dialog">×</button>
The visible × is not a name a screen reader can use.
4. States and relationships
<button type="button" aria-expanded="false" aria-controls="menu">Menu</button>
<ul id="menu" hidden>...</ul>
<input aria-describedby="email-hint" />
<p id="email-hint">We never share your email.</p>
<div role="dialog" aria-modal="true" aria-labelledby="dialog-title">
<h2 id="dialog-title">Confirm deletion</h2>
</div>
aria-expandedreflects open/closed and must be updated in JavaScript.aria-controlsreferences the element being controlled.aria-describedbylinks hints and errors.aria-labelledbynames a region from visible text.aria-liveannounces dynamic changes (see the forms lesson).
5. Focus management
Keyboard users need focus to go somewhere sensible.
On open a dialog: move focus to the dialog (or its first control).
While open: trap focus inside (Tab cycles within).
On close: return focus to the element that opened it.
On route change: move focus to the <h1> or announce the new page.
const dialog = document.querySelector("[role=dialog]");
const focusable = dialog.querySelectorAll(
'a[href], button:not([disabled]), input, select, textarea, [tabindex]:not([tabindex="-1"])',
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
dialog.addEventListener("keydown", (event) => {
if (event.key !== "Tab") return;
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
});
6. tabindex
tabindex="0"— add to the natural tab order (use only for custom widgets).tabindex="-1"— focusable by script, not by Tab (dialogs, headings after navigation).tabindex="1+"— never. It breaks the natural order.
Common mistakes
role="button"without keyboard handlers.aria-hidden="true"on a container that still has focusable children.- Redundant ARIA on native elements (
<button role="button">). - A modal that does not trap focus, so
Tabescapes to the page behind it. - Icon buttons with no label.
Practice
Build the accessible modal in the assignment. Focus trapping, Escape, labels and focus return are the core of almost every accessible widget.