TL;DR
- Write useful alt text (or
alt=""when decorative). - Use semantic HTML and landmarks (
<main>,<nav>,<header>, proper headings). - Meet color contrast and never rely on color alone to convey meaning.
- Ensure full keyboard navigation and visible focus; manage focus on SPA changes and modals.
- Give real labels to form controls and clear, programmatic error messages.
1) Images: alt text that helps (or stays silent)
Good rules
- Describe the purpose of the image in context; skip “image of…”.
- If purely decorative, use empty alt:
alt=""(screen readers will ignore it). - Icons that act as buttons/links need an accessible name (
aria-label, text, or<title>on inline SVG). - Don’t duplicate nearby captions; keep alt concise.
Examples
<!-- Content image with purpose -->
<img src="/team/ali.jpg" alt="Ali Karim, Backend Lead" />
<!-- Decorative / spacer -->
<img src="/bg/line.svg" alt="" />
<!-- Icon button -->
<button aria-label="Search">
<svg aria-hidden="true" focusable="false">…</svg>
</button>
2) Semantic HTML & landmarks (structure = navigation)
- Use real elements:
<button>for actions,<a href>for navigation. Avoiddiv onClick. - Provide landmarks so users can jump around:
<header>,<nav>,<main>,<aside>,<footer>. - Keep a single
<h1>per page and a logical heading order (h2underh1, etc.). - Add a Skip to content link as the first focusable item.
Examples
<a class="skip-link" href="#main">Skip to content</a>
<header>…</header>
<nav aria-label="Primary">…</nav>
<main id="main">
<h1>Account Settings</h1>
<section aria-labelledby="profile">
<h2 id="profile">Profile</h2>
…
</section>
</main>
<footer>…</footer>
<!-- Correct control types -->
<a href="/pricing">Pricing</a> <!-- link → navigation -->
<button type="button">Add to cart</button> <!-- button → action -->
3) Color contrast & non‑color cues
- Aim for contrast ratios that meet common guidance: 4.5:1 for normal text and 3:1 for large text (≥18.66px bold or ≥24px regular).
- Don’t rely on color alone to signal state (e.g., errors, required fields). Add icons, text, underline, patterns, etc.
- Keep a visible focus indicator; don’t remove outlines. Prefer
:focus-visiblefor nicer styling.
Examples
/* Keep focus visible; customize instead of removing */
:focus { outline: 2px solid currentColor; outline-offset: 2px; }
:focus-visible { outline: 3px solid #005fcc; }
/* Link style: more than just color */
a { text-decoration: underline; }
<!-- Error with text & icon, not color alone -->
<p id="email-error" class="error">
⚠️ Enter a valid email address.
</p>
4) Keyboard navigation & focus management
- Every interactive control must be reachable with Tab/Shift+Tab and operable with Enter/Space/Arrow keys as appropriate.
- Never use
tabindex> 0; use native order ortabindex="0"for custom widgets. - In modals, trap focus inside, restore focus to the trigger when closing, and allow Esc.
- In SPAs, when route changes, move focus to the top of the new content (
<h1>or container).
Examples
// Move focus after client-side route change
document.getElementById("main-heading")?.focus();
<!-- Modal skeleton -->
<div role="dialog" aria-modal="true" aria-labelledby="modal-title">
<h2 id="modal-title" tabindex="-1">Confirm delete</h2>
<button>Cancel</button>
<button>Delete</button>
</div>
5) Forms: labels, hints, and errors that connect
- Label every input; placeholder is not a label.
- Use
for+idor wrap the input with<label>. - Use
aria-describedbyto associate help text and error messages with the control. - Mark required fields with the
requiredattribute (and indicate visually). - Group related options with
<fieldset>+<legend>.
Examples
<label for="email">Email</label>
<input id="email" name="email" type="email" autocomplete="email" required
aria-describedby="email-hint email-error" />
<div id="email-hint" class="hint">We’ll send receipts here.</div>
<p id="email-error" class="error" role="alert">Enter a valid email.</p>
<fieldset>
<legend>Delivery options</legend>
<label><input type="radio" name="delivery" value="standard" /> Standard</label>
<label><input type="radio" name="delivery" value="express" /> Express</label>
</fieldset>
Quick testing (3 minutes)
- Tab through your page: Can you reach/see every interactive element?
- Zoom to 200%: Does layout still work and text remain readable?
- Disable CSS briefly: Does the document order and headings make sense?
- Run an automated check (e.g., Lighthouse/axe) to catch low‑hanging issues.
- Try a quick screen reader sanity check (VO/NVDA): can you reach the main content and operate key flows?
Pitfalls & fast fixes
| Pitfall | Why it hurts | Fix |
|---|---|---|
| Images with missing/verbose alt | Noise or missing info | Write purposeful alt or alt="" if decorative |
| Clickable divs/spans | Not keyboard‑operable by default | Use <button>/<a> or add full keyboard semantics |
| Hidden focus (outline: none) | Keyboard users get lost | Keep and style focus (:focus-visible) |
| Low contrast text | Hard to read | Adjust colors to meet contrast guidance |
| Placeholder as label | Disappears on input | Use a real <label>; keep hint text separate |
Quick checklist
- [ ] Meaningful alt or
alt=""for every<img>. - [ ] Proper landmarks and heading hierarchy; include a Skip to content link.
- [ ] Contrast meets guidance; links are identifiable beyond color.
- [ ] Full keyboard support and visible focus; manage focus on route changes & modals.
- [ ] Labels on all inputs; errors announced and programmatically linked.
One‑minute adoption plan
- Add a Skip to content link and ensure
<main>exists. - Pass through all images: write alt or set
alt="". - Fix contrast on low‑contrast text and show a clear focus outline.
- Convert clickable
<div>s into buttons/links. - Wire up labels and error messaging on forms (with
aria-describedby).