Web Accessibility Fundamentals: Building for Everyone

A practical introduction to web accessibility — semantic HTML, keyboard navigation, ARIA done right, color contrast, and how to test. The changes that help real users and happen to help your SEO too.

Frontend Craft: Web Accessibility Fundamentals: Building for Everyone

Why this matters

Roughly one in six people lives with some form of disability. They use screen readers, navigate by keyboard, rely on high contrast, or need captions. When your site ignores them, you’re not just excluding users — in many jurisdictions you’re also creating legal liability, and you’re leaving SEO on the table, because the same signals that help assistive tech help search crawlers.

Accessibility (often shortened to a11y — “a”, 11 letters, “y”) isn’t a feature you bolt on. It’s a property of code written with a little care. Most of it is free if you build right from the start, and expensive to retrofit if you don’t.

Start with semantic HTML

The single biggest accessibility win costs nothing: use the right element for the job. Assistive technology understands HTML’s built-in meaning. It cannot understand a <div> pretending to be a button.

<!-- ❌ A div that looks clickable but is invisible to assistive tech -->
<div class="button" onclick="submit()">Submit</div>

<!-- ✅ A real button: focusable, keyboard-operable, announced as a button -->
<button onclick="submit()">Submit</button>

That <button> gives you, for free: keyboard focusability, Enter/Space activation, and a screen reader announcing “Submit, button.” The <div> gives you none of it — you’d have to reimplement tabindex, key handlers, and ARIA roles just to limp back to what the native element did out of the box.

The pattern generalizes:

<nav>      <!-- navigation landmark, jumpable -->
<main>     <!-- the main content, jumpable -->
<header>, <footer>, <article>, <aside>
<h1>–<h6>  <!-- document structure screen readers navigate by -->
<button>   <!-- actions -->
<a href>   <!-- navigation to a URL -->
<label>    <!-- ties text to a form control -->

Reach for <div>/<span> only when no semantic element fits. They carry zero meaning — which is exactly right for pure styling hooks and exactly wrong for interactive controls.

Heading structure is navigation

Screen reader users navigate by headings the way sighted users skim — jumping heading to heading to find what they want. That only works if headings form a logical outline.

<!-- ❌ Levels chosen for font size — outline is nonsense -->
<h1>Page Title</h1>
<h4>Section</h4>       <!-- skipped h2, h3 -->
<h2>Subsection</h2>    <!-- jumps back up -->

<!-- ✅ Levels reflect structure; style with CSS -->
<h1>Page Title</h1>     <!-- one per page -->
  <h2>Section</h2>
    <h3>Subsection</h3>
  <h2>Another Section</h2>

Rules: one <h1> per page (the title), never skip levels going down, and choose the level for structure, not appearance. If <h2> looks too big, fix it in CSS — don’t reach for <h4> because it happens to be smaller.

Keyboard navigation

Many people can’t or don’t use a mouse — motor disabilities, screen reader users, and plenty of power users. Everything interactive must work with the keyboard alone.

Test it yourself right now: put the mouse down and Tab through your site. Can you reach every link, button, and field? Activate them with Enter/Space? See where focus is at all times? If focus vanishes or you can’t reach something, that’s a blocker for real users.

Never destroy focus indicators:

/* ❌ The single most common a11y crime — keyboard users are now lost */
*:focus { outline: none; }

/* ✅ Style it to match your design, but keep it visible */
:focus-visible {
  outline: 2px solid #3B82F6;
  outline-offset: 2px;
}

:focus-visible is the modern tool: it shows the ring for keyboard focus but not for mouse clicks, so you get a clean look for mouse users and a visible indicator for keyboard users. There’s no reason left to remove outlines entirely.

Manage focus on dynamic UI. Open a modal → move focus into it and trap it there. Close it → return focus to the element that opened it. Focus left stranded behind a modal, or lost to the page top after an action, disorients keyboard and screen reader users badly.

Images and alt text

Every meaningful image needs a text alternative describing its content or function, not its appearance:

<!-- Informative: describe what it conveys -->
<img src="chart.png" alt="Revenue grew 40% from Q1 to Q2 2026">

<!-- Functional (a link/button): describe the action -->
<a href="/cart"><img src="cart.svg" alt="View shopping cart"></a>

<!-- Decorative: empty alt so screen readers skip it -->
<img src="divider.svg" alt="">

The empty alt="" is deliberate and important: it tells the screen reader “skip this, it’s decoration.” Omitting alt entirely is different — many screen readers then read the filename aloud (“cart underscore icon dot svg”), which is worse than nothing. Decorative images get alt=""; they don’t get no alt.

Write alt text as if describing the image to someone on the phone: what does it show, or what does it do? “Chart” is useless; the trend the chart shows is the point.

Forms: label everything

Every input needs an associated label. Placeholder text is not a label — it vanishes when the user types, fails contrast requirements, and isn’t reliably announced.

<!-- ❌ Placeholder as label — gone the moment they type -->
<input type="email" placeholder="Email">

<!-- ✅ Real label, associated by id -->
<label for="email">Email address</label>
<input type="email" id="email" name="email">

The for/id link means clicking the label focuses the input (a bigger tap target — helps everyone, especially motor and touch users) and the screen reader announces the label when focus lands.

Announce errors, don’t just color them red:

<label for="email">Email address</label>
<input type="email" id="email" aria-invalid="true"
       aria-describedby="email-error">
<span id="email-error" role="alert">
  Please enter a valid email address
</span>

aria-describedby ties the error to the field so it’s read out with it; role="alert" makes the message announce the moment it appears. Color alone excludes colorblind users and anyone using a screen reader — the error has to exist in text, programmatically linked.

Color and contrast

Text must contrast enough with its background to be readable by people with low vision or color deficiencies. WCAG AA — the practical baseline — requires:

  • 4.5:1 for normal text
  • 3:1 for large text (18pt+, or 14pt+ bold)
/* ❌ Light gray on white — ~2.3:1, fails, unreadable for many */
.subtle { color: #aaaaaa; background: #ffffff; }

/* ✅ ~7:1, comfortably passes AA (and AAA) */
.subtle { color: #595959; background: #ffffff; }

That trendy light-gray body text is a real barrier. Check ratios with your browser’s dev tools (the color picker shows the contrast ratio and pass/fail) or WebAIM’s contrast checker.

And never rely on color alone to carry meaning:

<!-- ❌ A colorblind user sees no difference -->
<span style="color: red">Failed</span>
<span style="color: green">Passed</span>

<!-- ✅ Color plus a text/symbol cue -->
<span style="color: #c0392b">✗ Failed</span>
<span style="color: #27ae60">✓ Passed</span>

Red/green colorblindness is common; a status shown only by hue is invisible to those users. Add an icon, label, or shape.

ARIA: powerful, and easy to misuse

ARIA attributes add semantics HTML can’t express — but the first rule of ARIA is don’t use ARIA if native HTML can do it. Bad ARIA is worse than none, because it actively lies to assistive tech.

<!-- ❌ Redundant/harmful — button already has button semantics -->
<button role="button" aria-label="Submit">Submit</button>

<!-- ✅ Native semantics, nothing to add -->
<button>Submit</button>

<!-- ✅ ARIA earning its place: an icon-only button with no visible text -->
<button aria-label="Close dialog">
  <svg><!-- an X icon --></svg>
</button>

Legitimate ARIA jobs: labeling icon-only controls (aria-label), live regions that announce dynamic updates (aria-live), and marking state on custom widgets (aria-expanded, aria-selected). But a <div role="button"> you built by hand must also be focusable and handle Enter/Space and reflect its state — all the things a real <button> gave you for free. The lesson repeats: use the native element and skip the ARIA entirely.

Respect motion preferences

Animation can trigger nausea, dizziness, even seizures for some users. Honor their system setting:

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

Users who’ve asked their OS for less motion get a calm experience; everyone else keeps the animations. One media query, real harm avoided.

How to actually test

Automated catches the mechanical ~30–40% — missing alt, contrast failures, absent labels:

  • axe DevTools (browser extension) — the standard
  • Lighthouse — the Accessibility audit in Chrome dev tools
  • eslint-plugin-jsx-a11y — flags issues in React as you write

Automated tools are necessary and nowhere near sufficient. They can’t tell whether your alt text is meaningful or your focus order makes sense.

Manual catches what matters most:

  • Keyboard-only — unplug the mouse, Tab through everything.
  • Screen reader — VoiceOver (Mac, built in), NVDA (Windows, free), or TalkBack (Android). Genuinely eye-opening the first time; you hear your own UI the way a blind user does.
  • Zoom to 200% — does the layout hold or does content get cut off?
  • The contrast checker on your actual colors.

Fifteen minutes of keyboard-and-screen-reader testing finds problems no automated tool ever will.

The payoff beyond compliance

Accessible sites are better sites, full stop. Semantic HTML helps search engines parse your content. Keyboard support helps power users. Captions help people in noisy places and those learning the language. High contrast helps everyone in bright sunlight. Clear labels reduce mistakes for all users.

You’re not building a special version for a minority. You’re building a robust version that works in more situations — which is just good engineering. Start with semantic HTML and keyboard support, keep contrast honest, test with the keyboard and a screen reader, and you’ll clear most of the bar without ever treating accessibility as a separate task.