Accessibility
Accessibility means the page works for people who are not using it the way you are: with a screen reader, with a keyboard and no mouse, at 200% zoom, with a tremor, with colour vision deficiency, with the sound off, in a hurry on a cracked phone screen.
Almost all of it is choosing elements that already do the work. This lesson is the specifics.
The four principles
WCAG - the Web Content Accessibility Guidelines - organises everything under four words, and they are a useful checklist even if you never read the standard.
Perceivable. Can the information get in? Text alternatives for images, captions for video, enough contrast, not relying on colour alone.
Operable. Can it be used? Everything reachable by keyboard, no traps, enough time, nothing that flashes.
Understandable. Is it predictable? Consistent navigation, clear labels, errors that say what to do.
Robust. Does it work with the tools people actually use? Valid markup, correct semantics, no reliance on one browser's behaviour.
Headings, links and images
Three things carry most of the weight, and all three were covered earlier - they are repeated here because they are where real audits find problems.
Headings are an outline. One h1, no skipped levels, and chosen by level rather than by size. Screen reader users navigate by heading more than by anything else.
Link text makes sense alone. "Read the accessibility guide", not "click here". The list of links on a page is a thing people actually read.
Images say what they show. Informative images get the information, images inside links get the destination, decorative images get alt="".
<main>
<h1>Course handbook</h1>
<h2>Enrolment</h2>
<p>See the <a href="/enrolment">enrolment process</a> for deadlines.</p>
<h3>Deadlines</h3>
<p>
<img src="calendar.svg" alt="" width="24" height="24">
Applications close on 14 March.
</p>
</main>
Open this example in the editor and change it. Nothing you type is saved.
The calendar icon has alt="" because the sentence next to it already says what it says. An icon that repeats adjacent text is decoration.
Forms and the keyboard
<form action="/apply" method="post">
<p>
<label for="name">Full name</label>
<input id="name" name="name" type="text" autocomplete="name" required>
</p>
<fieldset>
<legend>Study mode</legend>
<p>
<input id="full-time" name="mode" type="radio" value="full" checked>
<label for="full-time">Full time</label>
</p>
<p>
<input id="part-time" name="mode" type="radio" value="part">
<label for="part-time">Part time</label>
</p>
</fieldset>
<p>
<label for="ref">Reference number</label>
<input id="ref" name="ref" type="text"
aria-describedby="ref-help" pattern="[A-Z]{3}-[0-9]{4}"
title="Three capital letters, a hyphen, then four digits">
<span id="ref-help">For example, OMN-4821. Leave blank if you do not have one.</span>
</p>
<button type="submit">Apply</button>
</form>
Open this example in the editor and change it. Nothing you type is saved.
Every control has a real label. The radio group has a fieldset and legend, so the question is announced along with the options. aria-describedby attaches the help text, which is then read after the label rather than instead of it - the distinction title cannot make.
Then test it with the keyboard, which takes two minutes and finds more than any tool:
- Tab through every control. Anything you cannot reach is unusable for a lot of people.
- Watch the focus ring. If you cannot see where you are, neither can they. Never write
outline: nonewithout putting something else in its place. - Check the order matches the visual order.
- Make sure you can get out of everything you can get into. A focus trap outside a modal is a dead end.
ARIA, and when not to
ARIA adds roles, names, states and properties to elements that do not carry them natively.
The first rule of ARIA, from the specification: do not use ARIA if a native element will do. A button is focusable, activates on Enter and Space, exposes a disabled state and announces itself. A div role="button" gives you the announcement only, and you now owe tabindex, two key handlers and a focus style.
The attributes worth knowing are small in number:
<nav aria-label="Breadcrumb">
<ol>
<li><a href="/">Home</a></li>
<li><a href="/courses">Courses</a></li>
<li><a href="/courses/html" aria-current="page">HTML</a></li>
</ol>
</nav>
<button type="button" aria-expanded="false" aria-controls="filters">
Filters
</button>
<div id="filters" hidden>
<p>The filter controls.</p>
</div>
<p aria-live="polite" id="status">Nothing to report.</p>
Open this example in the editor and change it. Nothing you type is saved.
aria-label names something that has no visible label. aria-labelledby points at an element that already contains the name. aria-describedby attaches extra detail. aria-current marks which item in a set is the current one. aria-expanded reports whether a disclosure is open - and it has to be updated when it toggles, or it is worse than absent. aria-live on a region announces changes to it without moving focus.
Bad ARIA is worse than none, because it overrides what the element really is. role="button" on a link makes a screen reader announce "button" for something that navigates.
Colour, contrast and motion
Contrast. Body text needs a ratio of at least 4.5 to 1 against its background; large text needs 3 to 1. Every browser's developer tools will measure it.
Never colour alone. A required field marked only in red, an error shown only by a red border, a chart distinguished only by hue - each is invisible to a substantial number of people. Add a symbol, a label, a pattern.
Motion. Anything that animates should respect prefers-reduced-motion, and nothing should flash more than three times a second.
Hiding things, four different ways
"Hidden" is not one state, and choosing the wrong one is a common cause of a page that works visually and not otherwise.
<p hidden>Hidden from everybody: not drawn, not announced, not focusable.</p>
<p class="visually-hidden">Visible to a screen reader, not drawn on screen.</p>
<p aria-hidden="true">Drawn on screen, not announced. For decoration only.</p>
<div inert>
<p>Still drawn and announced, but not interactive:
<a href="/somewhere">this link cannot be reached or clicked</a>.</p>
</div>
Open this example in the editor and change it. Nothing you type is saved.
hidden - and CSS display: none - removes an element from everything.
The visually-hidden class is the opposite: a CSS pattern that keeps an element in the accessibility tree while taking it off screen. It is how a search box gets a real label without showing one, and how a skip link stays invisible until focused.
aria-hidden="true" hides from assistive technology only. Correct for a decorative icon beside text that already says the same thing; wrong for anything focusable, because a screen reader user can still reach it with Tab and will be told nothing about it.
inert makes a subtree non-interactive while leaving it visible - what a modal dialog needs for everything behind it, and what showModal() applies for you.
Testing
Automated tools catch perhaps a third of real problems, and they catch the mechanical ones: missing alt, missing labels, low contrast, duplicate ids. Run one - the Lighthouse accessibility audit in Chrome is built in - and fix what it reports.
Then do the parts it cannot:
- Unplug the mouse and use the page.
- Zoom to 200% and check nothing is lost.
- Turn on a screen reader. VoiceOver on a Mac is Cmd-F5, Narrator on Windows is Ctrl-Windows-Enter. Listen to one form and one table.
- Read the page with CSS off.
The last word belongs to people who use these tools daily: no checklist substitutes for testing with actual users.
Practice task
Run Lighthouse against a page you have built and fix everything it reports. Then do the manual passes: keyboard only, 200% zoom, CSS off. Write down what the tool missed - that list is the one worth keeping, because it is the same list on every page you will ever build.