Accessible Components: The Five Things That Matter Most
You don't need to memorise WCAG. Five habits cover the overwhelming majority of real-world accessibility failures.
Accessibility gets treated as a specialist discipline with a 400-page spec, which is how it ends up deferred forever. In practice, a small number of mistakes account for most of the damage. Fix these five and you're ahead of most production software.
1. Use the right element
Most accessibility bugs are a div doing a job HTML already has an element for.
<!-- not focusable, not keyboard-activatable,
not announced as a button, no default role -->
<div class="btn" onclick="submit()">Save</div>
<!-- all of that, free -->
<button type="button" onclick="submit()">Save</button>A <button> is focusable, activates on Enter and Space, announces itself as a button, participates in form submission, and shows a focus ring. Replicating that on a div takes tabindex, two keyboard handlers, role, aria-pressed where relevant, and custom focus styles — and you'll get one of them wrong.
Same for <a> versus a click-handling div. If it navigates, it's a link — and a link supports middle-click, ctrl+click, copy link address, and the browser's own history. A div supports none of that.
2. Label every input, visibly
<!-- placeholder is not a label: it disappears on focus,
fails contrast, and isn't reliably announced -->
<input placeholder="Email address" />
<!-- -->
<label for="email">Email address</label>
<input id="email" type="email" autocomplete="email" />Placeholder-as-label is a design trend that actively harms everyone: screen reader users get nothing, and sighted users forget which field they're in the moment they start typing. If space is genuinely tight, a floating label works — the text stays on screen.
While you're there: autocomplete attributes. They're an accessibility feature — for people with motor impairments, for anyone on a phone, and honestly for everyone.
3. Never remove the focus ring
/* the single most damaging line in web CSS */
*:focus { outline: none; }
/* if the default is ugly, replace it — don't delete it */
:focus-visible {
outline: 2px solid hsl(var(--ring));
outline-offset: 2px;
}For a keyboard user, the focus ring is the cursor. Removing it is exactly as usable as hiding the mouse pointer.
:focus-visible is the answer to the objection that motivated the deletion in the first place — it shows the ring for keyboard navigation and not for mouse clicks. That was the entire complaint, and it's been solved for years.
4. Manage focus when the UI changes
This is the one people miss, because it's invisible if you use a mouse. When a modal opens, focus must move into it, be trapped inside it, and return to the trigger when it closes. Otherwise a keyboard user opens a dialog and their focus is still behind it, tabbing through content they can't see.
- Modal opens → focus the dialog (or its first control). Trap Tab inside. Escape closes.
- Modal closes → focus returns to the element that opened it.
- Client-side route change → move focus to the new page's
<h1>, or a skip target. - Content removed → move focus somewhere sensible before removing it, or it lands on
<body>and context is lost. - Async result arrives → announce it in an
aria-liveregion. A spinner that silently becomes a table announces nothing.
The good news: a headless component library (Radix, Headless UI, Angular CDK) handles dialogs, menus, and comboboxes correctly. Use one. Focus trapping is genuinely fiddly and there's no prize for writing your own.
5. Don't encode meaning in colour alone
A red border on an invalid field communicates nothing to someone with colour blindness — roughly 1 in 12 men. Pair colour with a second channel: an icon, a text label, a pattern.
<input id="ssn" aria-invalid="true" aria-describedby="ssn-error" />
<p id="ssn-error" class="error">
<AlertCircle aria-hidden="true" />
Enter a 9-digit number, no dashes.
</p>Colour, icon, and text. Also note the error text says what to do — "Invalid input" tells the user they failed without telling them how to succeed.
How to check in five minutes
- 01Put your mouse away and Tab through the page. Can you reach and operate everything? Can you always see where focus is?
- 02Zoom to 200%. Does anything overlap or get cut off?
- 03Run axe DevTools. It catches contrast, missing labels, and bad ARIA automatically.
- 04Turn on your OS screen reader and listen to one flow. Uncomfortable the first time, permanently clarifying.
Step 1 alone finds most of it. If you can't complete your app's main flow with a keyboard, nothing else on this list matters yet.