🎓 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.

✕ Inaccessible What to avoid ✓ Accessible Do this instead
01

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.

02

Use real buttons

A styled <div> looks like a button but can't be reached or operated with a keyboard.

WCAG 2.1.1 Keyboard · 4.1.2 Name, Role, Value

✕ Inaccessible

Try to reach & activate this with the keyboard — you can't.

Add to cart

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.

03

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.

04

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 — fails

Light-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 AAA

Dark 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.

05

Meaningful alt text

Screen readers read alt aloud. Missing or junk alt text leaves blind users guessing.

WCAG 1.1.1 Non-text Content

✕ 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="".

06

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.

08

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 h2h3 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.

09

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.

10

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?

Focus is trapped here. Try Tab, then press Esc — focus returns to the button.

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 →