🎓 Accessibility training
Learn accessibility by example
Each lesson below shows the same component built two ways: an inaccessible version and an accessible one. The demos are live — try them with your keyboard (use Tab, Enter and Space) and, if you can, a screen reader. Then read what changed and why it matters.
Visible focus indicators
Keyboard users need to see where they are. Removing the focus outline leaves them lost.
WCAG 2.4.7 Focus Visible (AA) · 2.4.13 Focus Appearance (AAA)✕ Inaccessible
Press Tab through these — can you tell which is focused?
outline: none with no replacement hides focus entirely.
✓ Accessible
Press Tab — a clear ring follows you.
A high-contrast :focus-visible ring keeps every control discoverable.
Show the code
✕ Avoid CSS
button:focus {
outline: none;
}
Hides the focus ring for every keyboard user.
✓ Better CSS
button:focus-visible {
outline: 3px solid #1a73e8;
outline-offset: 2px;
}
A high-contrast ring, shown only when navigating by keyboard.
Use real buttons
A styled <div> looks like a button but can't be reached or operated with a keyboard.
✕ Inaccessible
Try to reach & activate this with the keyboard — you can't.
Cart: 0
A <div onclick> has no role, isn't focusable, and ignores Enter/Space.
✓ Accessible
Tab to it, press Enter or Space — it works.
Cart: 0
A native <button> is focusable, announced as a button, and keyboard-operable for free.
Show the code
✕ Avoid
<div class="btn"
onclick="add()">Add</div>
Not focusable; ignores Enter/Space.
✓ Better
<button type="button"
onclick="add()">Add</button>
Focusable, keyboard-operable and announced as a button.
Labelled fields & clear errors
Placeholders aren't labels, and colour alone can't communicate an error.
WCAG 1.3.1 · 3.3.1 Error Identification · 3.3.2 Labels · 1.4.1 Use of Colour✕ Inaccessible
Submit empty. Errors are red-only, with no labels.
No <label>, placeholders vanish on typing, and errors rely on colour with no text.
✓ Accessible
Submit empty. Errors are announced, with text + icon.
Real labels, aria-describedby errors, a focus-managed summary, and errors shown with an icon — not colour alone.
Show the code
✕ Avoid
<input placeholder="Email">
No label; the placeholder vanishes on typing.
✓ Better
<label for="email">Email</label>
<input id="email" type="email"
aria-describedby="err">
<span id="err">Enter a valid email.</span>
Real label, and the error is tied to the field with aria-describedby.
Sufficient colour contrast
Low-contrast text is hard to read in sunlight, on cheap screens, or with low vision.
WCAG 1.4.3 Contrast (Minimum) — 4.5:1 for body text✕ Inaccessible
Subscribe to our newsletter for weekly tips and updates.
Contrast ≈ 1.9:1 — failsLight-grey text on a grey card is decorative at best and unreadable for many.
✓ Accessible
Subscribe to our newsletter for weekly tips and updates.
Contrast ≈ 13:1 — passes AAADark text on a light surface clears AA (4.5:1) comfortably and stays legible everywhere.
Show the code
✕ Avoid CSS
.note {
color: #a3a3a3; /* on #8e8e8e */
} /* ≈ 1.9:1 — fails */
Light grey on grey is unreadable for many.
✓ Better CSS
.note {
color: #1a1a1a; /* on #f4f4f0 */
} /* ≈ 13:1 — passes */
Dark on light clears AA (4.5:1) with room to spare.
Meaningful alt text
Screen readers read alt aloud. Missing or junk alt text leaves blind users guessing.
✕ Inaccessible
A screen reader announces:
"image 4F7A9C.jpg"
No alt falls back to the filename; alt="image" is just noise.
✓ Accessible
A screen reader announces:
"Aerial view of a Maldivian island with turquoise lagoon"
Describe the purpose. Purely decorative images get alt="" so they're skipped.
Show the code
✕ Avoid
<img src="island.jpg">
No alt — a screen reader reads the filename.
✓ Better
<img src="island.jpg"
alt="Aerial view of a Maldivian
island and turquoise lagoon">
Describe the purpose. Decorative images get alt="".
Large enough touch targets
Tiny, crowded controls are hard to tap — especially with tremors or large fingers.
WCAG 2.5.8 Target Size (Minimum) — at least 24×24px, ideally 44×44px✕ Inaccessible
Try tapping just one icon on a phone.
~20px targets with no spacing cause mis-taps and frustration.
✓ Accessible
Comfortable to hit, first time.
44×44px targets with spacing are easy and forgiving for everyone.
Show the code
✕ Avoid CSS
.icon-btn {
width: 20px;
height: 20px;
}
~20px targets cause mis-taps.
✓ Better CSS
.icon-btn {
min-width: 44px;
min-height: 44px;
}
44×44px (min 24×24 for WCAG 2.5.8 AA) is easy to hit.
Descriptive link text
Many people skim by jumping link to link. "Click here" out of context says nothing.
WCAG 2.4.4 Link Purpose (In Context)✕ Inaccessible
A screen reader's "list of links" would read:
Three identical, meaningless links. Out of context, none tell you where they go.
✓ Accessible
The same list, now self-explanatory:
The link text alone makes the destination obvious — no surrounding sentence required.
Show the code
✕ Avoid
<a href="/report.pdf">click here</a>
Meaningless out of context, e.g. in a links list.
✓ Better
<a href="/report.pdf">
Download the 2025 report (PDF)
</a>
The link text alone says where it goes.
Logical heading structure
Screen-reader users navigate by headings. Skipped levels or fake "headings" break that map.
WCAG 1.3.1 Info & Relationships · 2.4.6 Headings & Labels✕ Inaccessible
Bold text that only looks like headings:
Our Services
Web Audits
Training
A styled <p> carries no heading role and is invisible to heading navigation. Jumping from level 1 to 3 also skips a level.
✓ Accessible
Real, properly nested headings:
h2 Our Services
h3 Web Audits
h3 Training
Convey structure with real headings: nest h2 → h3 in order without skipping levels, and style them with CSS — never pick a tag for its size. A single h1 for the page's main topic is a solid convention (multiple h1s aren't an automatic failure, but one is easier to reason about).
Show the code
✕ Avoid
<p class="title">Our services</p>
<p class="sub">Web audits</p>
Styled paragraphs carry no heading role.
✓ Better
<h2>Our services</h2>
<h3>Web audits</h3>
Real, properly nested headings — styled with CSS, not tag size.
Name your icon-only buttons
An icon button with no text leaves a screen reader with nothing to announce but "button".
WCAG 4.1.2 Name, Role, Value · 1.1.1 Non-text Content✕ Inaccessible
Announced as:
"button … button … button"
The emoji is decorative to the accessibility tree, so each control is just an unnamed "button".
✓ Accessible
Announced as:
"Search, button … Notifications, button … Settings, button"
An aria-label (or visually-hidden text) gives each button a clear, spoken name.
Show the code
✕ Avoid
<button>🔍</button>
Announced only as “button”.
✓ Better
<button aria-label="Search">🔍</button>
aria-label gives the icon button a spoken name.
Modal dialogs & focus management
A real dialog traps focus, closes on Esc, and returns focus when dismissed.
WCAG 2.4.3 Focus Order · 2.1.2 No Keyboard Trap · 1.3.1✕ Inaccessible
Open it, then press Tab — focus drifts to the page behind.
A plain <div> overlay has no dialog role, doesn't trap or restore focus, and ignores Esc.
✓ Accessible
Open it — focus moves inside, Esc closes, focus returns.
The native <dialog> element handles focus trapping, Esc, and focus return for you.
Show the code
✕ Avoid
<div class="overlay" id="m">…</div>
open.onclick = () =>
m.style.display = "block";
No focus trap, no Esc, focus lost on close.
✓ Better
<dialog id="m">…</dialog>
open.onclick = () =>
m.showModal();
The native <dialog> traps focus, handles Esc and restores focus.
Subscribe?
Notice how Tab still reaches the page behind this overlay.
▶ Test it yourself
You don't need fancy tools to catch most issues. Start here:
⌨️ Unplug your mouse
Navigate the whole page with Tab, Shift+Tab, Enter, Space and arrow keys. Can you reach and use everything? Can you always see focus?
🔊 Turn on a screen reader
VoiceOver (⌘+F5 on Mac) or NVDA (free, Windows). Close your eyes and listen — does it make sense?
🔍 Zoom to 200%
Press Ctrl/⌘ + a few times. Does content reflow without horizontal scrolling or clipping?
🧪 Run axe DevTools (opens in a new tab)
A free browser extension that catches many issues automatically. Great first pass — but no tool replaces manual testing.
Ready to write it yourself? See the actual accessible markup — semantic HTML, ARIA, forms, focus and live regions — with copy-paste code.
Accessible code by example →