Accessibility
Same five bugs in every audit: div buttons, missing labels, broken headings
Tab through your product. Can you see focus? Can you open and close a dialog without a mouse?
Semantic HTML before ARIA
<button> has role, keyboard handling, and form-submit built in. A <div role="button" tabIndex={0}> reproduces all of that worse.
- Clickable
<div>. Use<button>. If it navigates, use<a href>. - Custom dropdowns. Use
<details>/<summary>. - Custom checkboxes. Style
<input type="checkbox">. Don't replace it. <div role="navigation">. Use<nav>.- Heading text in
<div class="title">. Use<h1>-<h3>.
ARIA is for tab panels, live regions, custom comboboxes. Reach for it second.
Icon buttons need aria-label
- Visible text wins. Don't add
aria-labelif text already exists. - Decorative icons get
aria-hidden. - Be specific. "Delete this comment" beats "Delete."
- Never contradict.
aria-label="Cancel"on a Submit button is a bug.
Skip links
Heading hierarchy
- One
<h1>per page. - Never skip levels.
<h1>then<h3>breaks the outline. - Headings describe sections. "Billing history," not "Section 1."
- Don't use headings for styling. Use CSS for big bold text.
Contrast
WCAG: 4.5:1 body text, 3:1 large text and UI elements.
Never rely on colour alone:
- Error fields: colour + icon + label + role.
- Required fields: colour + asterisk + SR-only text.
- Active tab: colour + underline or weight change.
:focus-visible
Never apply outline: none without a replacement.
Common mistakes
- Click handler on a
<div>. Mouse-only. Use<button>. outline: nonewith no replacement. Keyboard users are blind.- Skip link that CSS hid from focus. Test by tabbing from page load.
aria-labelcontradicting visible text. Confuses everyone.tabIndex="-1"everywhere. Removes from tab order. Use only for programmatic focus.autocomplete="off"on passwords. Breaks password managers.