Lesson 15 • Expert Track
Web Accessibility (A11y)
By the end of this lesson you'll be able to build pages that work for keyboard users, screen-reader users, and people with low vision — and you'll be able to spot and fix inaccessible markup on sight.
What You'll Learn
<label> feel new, review CSS Basics & Selectors first.💡 Think of It Like This
Accessibility is like a building with ramps, lifts, and braille signs. A staircase works fine if you can walk — but a ramp lets everyone in, including the person in a wheelchair, the parent with a pushchair, and the courier with a heavy trolley.
Your webpage is the building. A screen reader is a blind visitor who can only "see" the page through the labels and structure you provide. If your "button" is really a styled <div>, it's like a door with no handle — some visitors simply can't open it. Over 1 billion people worldwide live with a disability, so these ramps are not an edge case.
1. Why Accessibility Matters
Accessibility (often shortened to a11y — "a", then 11 letters, then "y") means building pages that work for people who use assistive technology: screen readers, keyboard-only navigation, screen magnifiers, and more. It is not a nice-to-have bolted on at the end; it is part of writing correct HTML.
| Reason | Why you should care |
|---|---|
| 🧑🤝🧑 Ethical | Around 15% of the world lives with a disability. The web should work for them too. |
| ⚖️ Legal | Many countries require WCAG compliance (ADA in the US, EAA in the EU). Lawsuits are real. |
| 📈 SEO | Semantic HTML and alt text are exactly what search engines read to rank you. |
| 👥 UX | Accessible sites are clearer and faster for everyone, not just disabled users. |
2. Semantic HTML — The Foundation
The single most important thing for accessibility is to use the right HTML element for the job. A screen reader knows that <button> is clickable, that <nav> is navigation, and that <main> is the main content — but a <div> is meaningless furniture it has to skip past. "Semantic" just means the tag describes what the thing is, not how it looks.
| ❌ Inaccessible | ✅ Accessible |
|---|---|
<div onclick="..."> | <button> |
<div class="nav"> | <nav> |
<div class="header"> | <header> |
<b>Title</b> | <h1>Title</h1> |
Run the worked example below. The same visual layout is built twice — once from <div>s and once from real elements. They look identical, but only the second one tells a screen reader what each part is.
Worked example: inaccessible vs accessible markup
Same look, very different experience for a screen reader
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Semantic HTML</title>
<style>
body { font-family: system-ui, sans-serif; background:#0f172a; color:#e5e7eb; padding:20px; }
.panel { background:#1e293b; border-radius:10px; padding:16px; margin:14px 0; }
.fake-btn, .real-btn {
display:inline-block; background:#3b82f6; color:white;
padding:10px 18px; border-radius:8px; border:none;
font:inherit; cursor:pointer; margin-top:8px;
}
h2 {
...3. Heading Order & Alt Text
Screen-reader users often jump heading to heading to scan a page, the way a sighted reader skims bold titles. That only works if your headings form a logical outline: one <h1> per page, then <h2> for each section, then <h3> under those. Never skip a level just to get a smaller font — that's a job for CSS, not for picking the wrong tag.
For images, the alt attribute is the text a screen reader speaks in place of the picture. Get it right by image type:
| Image type | Rule | Example |
|---|---|---|
| Informative | Describe what it shows | alt="Golden retriever catching a ball" |
| Decorative | Empty alt (present, not missing!) | alt="" |
| Functional (logo/icon link) | Describe the action/destination | alt="Acme home page" |
Worked example: heading order and alt text
A correct outline plus the three kinds of alt text
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Headings & alt text</title>
<style>
body { font-family: system-ui, sans-serif; background:#0f172a; color:#e5e7eb; padding:20px; }
img { border-radius:8px; background:#334155; }
h1 { color:#3b82f6; }
h2 { color:#22c55e; }
h3 { color:#f59e0b; }
</style>
</head>
<body>
<!-- One h1 = the page title. Screen reader: "heading level 1". -->
<h1>Dog Breeds</h1>
<!-- h2 = a section under the h1. Outli
...4. Keyboard Navigation & Visible Focus
Many people never touch a mouse. They move through a page with Tab (next control), Shift+Tab (previous), Enter/Space (activate), and Escape (close). For this to work, every interactive element must be reachable and must show a visible focus indicator — the ring that tells you where you are. Native elements (<a>, <button>, <input>) are focusable for free; that's another reason to use them.
/* ✅ A clear focus ring for keyboard users */
:focus-visible {
outline: 3px solid #3b82f6; /* high-contrast, easy to spot */
outline-offset: 2px;
}
/* ❌ NEVER remove the outline with no replacement */
/* :focus { outline: none; } <- keyboard users are now lost */The demo below is a full accessible page: a skip link, semantic landmarks, a labelled form, and a bright focus ring. Open it and press Tab repeatedly — watch the skip link appear first, then the focus ring jump from control to control.
Worked example: a fully accessible page
Skip link, landmarks, labels, and visible focus — press Tab to explore
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Accessible Page</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background:#0f172a; color:#e5e7eb; }
/* Skip link: hidden off-screen until it receives focus, then slides in. */
.skip-link {
position:absolute; top:-60px; left:10px; z-index:100;
background:#3b
...5. Colour Contrast (WCAG AA)
Low-contrast text — pale grey on white, light blue on teal — is hard to read for everyone and impossible for many people with low vision. WCAG sets a measurable target called a contrast ratio, from 1:1 (invisible) to 21:1 (black on white). To pass AA, the level most laws require, hit these numbers:
| Level | Normal text | Large text (18pt+/24px) |
|---|---|---|
| AA (required) | 4.5:1 | 3:1 |
| AAA (best practice) | 7:1 | 4.5:1 |
💡 Pro tip: Don't guess. Open browser DevTools, inspect a text element, and the colour picker shows the live contrast ratio with a pass/fail tick against AA and AAA.
And never rely on colour alone to convey meaning — a red/green status dot is invisible to colour-blind users. Add an icon, a word, or a shape as well.
🎯 Your Turn #1 — Fix the fake button
This "card" uses a <div> as a button and an image with no alt text. Replace the blanks so it becomes accessible, then run it and check the expected screen-reader behaviour written in the comments.
Your Turn #1: make the control real
Swap the div-button for a <button> and give the image alt text
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Your Turn 1</title>
<style>
body { font-family: system-ui, sans-serif; background:#0f172a; color:#e5e7eb; padding:20px; }
.card { background:#1e293b; border-radius:10px; padding:16px; max-width:340px; }
.buy {
display:inline-block; background:#22c55e; color:#0f172a;
padding:10px 18px; border-radius:8px; border:none;
font:inherit; font-weight:700; cursor:pointer; margin-top:10px;
}
:f
...🎯 Your Turn #2 — Label the form
These inputs only have placeholder text — which vanishes when you type and is not a label. Connect each input to a <label> using matching for / id values, then verify the expected screen-reader behaviour in the comments.
Your Turn #2: connect labels to inputs
Add for/id so the screen reader announces each field's name
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Your Turn 2</title>
<style>
body { font-family: system-ui, sans-serif; background:#0f172a; color:#e5e7eb; padding:20px; }
label { display:block; margin:12px 0 4px; color:#cbd5e1; font-size:14px; }
input { width:100%; max-width:320px; padding:10px; border-radius:6px;
border:2px solid #334155; background:#1e293b; color:white; }
:focus-visible { outline:3px solid #3b82f6; outline-offset:2px; }
...🧩 Mini-Challenge — Build an accessible mini-page
Support is faded now — only an outline is given. Build a small page from scratch that ties the whole lesson together. Use the worked page in section 4 as your reference if you get stuck.
Mini-Challenge: accessible from scratch
Skip link, landmarks, one labelled input, a real button, a visible focus ring
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Mini-Challenge</title>
<style>
body { font-family: system-ui, sans-serif; background:#0f172a; color:#e5e7eb; padding:20px; }
/* 🧩 add a :focus-visible rule with a 3px outline */
</style>
</head>
<body>
<!-- 🧩 MINI-CHALLENGE: build a tiny but fully accessible newsletter page
1. First link on the page = a skip link pointing to #main
2. A <header> with an <h1> title and a <nav aria-label="Main">
...⚠️ Common Errors (and the fix)
- Missing alt text. Screen reader reads the file name out loud: "headphones-final-v2.png". Fix: add
alt="..."describing the image, oralt=""if it's purely decorative. - Div-buttons.
<div onclick="...">can't be reached by Tab, doesn't respond to Enter/Space, and isn't announced as a button. Fix: use<button>(or<a>if it navigates). - Low contrast. Pale grey text on white reads at ~1.6:1 and fails WCAG AA. Fix: darken the text (or lighten the background) until DevTools shows at least 4.5:1 for normal text.
- No labels. Inputs with only placeholder text are announced as an unnamed "edit text". Fix: add a
<label>withformatching the input'sid. - outline: none. Removing the focus ring with no replacement leaves keyboard users unable to see where they are. Fix: style
:focus-visiblewith a high-contrast outline instead.
📋 Quick Reference
| Goal | Use this |
|---|---|
| A clickable action | <button>…</button> |
| Navigation region | <nav aria-label="Main"> |
| Main content landmark | <main id="main"> |
| Informative image | <img src="…" alt="what it shows"> |
| Decorative image | <img src="…" alt=""> |
| Labelled input | <label for="x">…</label><input id="x"> |
| Skip link | <a href="#main">Skip to main content</a> |
| Visible focus ring | :focus-visible { outline: 3px solid #3b82f6 } |
| Contrast (AA, normal text) | at least 4.5:1 |
❓ Frequently Asked Questions
What is the single most important thing I can do for accessibility?
Use semantic HTML. Choosing the right element for the job — <button> for actions, <nav> for navigation, <h1>–<h6> for headings, <main> for the main content — gives screen readers, keyboards, and search engines built-in meaning for free. Most accessibility problems come from <div> and <span> used where a real element belongs.
What does WCAG AA mean and which contrast ratio do I need?
WCAG (Web Content Accessibility Guidelines) defines levels A, AA, and AAA. AA is the level most laws require. For AA, normal text needs a contrast ratio of at least 4.5:1 against its background, and large text (18pt/24px, or 14pt/18.66px bold) needs at least 3:1.
Is it ever OK to write outline: none on focus?
Only if you immediately provide a clearly visible replacement focus style. Removing the outline with nothing in its place makes the page impossible to use with a keyboard, because the user can no longer see where they are. The safe pattern is to style :focus-visible with a high-contrast outline instead.
When should an image have an empty alt attribute (alt="")?
Use alt="" for purely decorative images that add no information — background flourishes, spacer images, icons sitting next to text that already says the same thing. The empty alt tells the screen reader to skip the image. This is different from leaving alt off entirely, which makes the screen reader announce the file name, which is a real bug.
How do I connect a label to a form input?
Give the input an id and point a <label> at it with a matching for attribute: <label for="email">Email</label><input id="email">. The screen reader then announces the label when the field is focused, and clicking the label focuses the input. Placeholder text is not a label and disappears as soon as the user types.
What is a skip link and why do I need one?
A skip link is the first focusable link on the page ("Skip to main content") that jumps past the repeated header and navigation straight to <main>. Keyboard and screen-reader users would otherwise have to Tab through every nav link on every page. It is usually hidden until it receives focus.
🎉 Lesson Complete
You can now build pages that work for everyone. The essentials:
- ✅ Reach for semantic HTML first — it's the #1 accessibility win
- ✅ Give images real alt text (or
alt=""when decorative) - ✅ Keep headings in order — one h1, then h2, then h3
- ✅ Make every control keyboard-operable with a visible focus ring
- ✅ Meet WCAG AA contrast — 4.5:1 for normal text
- ✅ Label every form control and add a skip link
Next up: HTML5 Semantic Architecture, where you'll structure whole pages with these landmark elements.
Sign up for free to track which lessons you've completed and get learning reminders.