# Accessibility From Day One

> Accessibility isn't a checklist bolted on at the end - it's a byproduct of using HTML correctly. Learn why it matters, how ARIA and focus management fill the gaps semantic HTML leaves, and how to test with a real screen reader.


---

# Accessibility From Day One

A blind developer uses a screen reader to shop online every week. A warehouse worker with a repetitive
strain injury navigates his team's internal tools by keyboard because a mouse hurts. Someone with low
vision has the browser zoomed to 200%. None of them are edge cases - they're a normal slice of your
user base, and most of what they need comes free from writing HTML the way it was designed to be
written.

This guide assumes you already know HTML, CSS, and forms from
[HTML From Zero](/guides/html-from-zero), [CSS Without Tears](/guides/css-without-tears), and
[Forms That Work](/guides/forms-that-work). It builds on that foundation instead of re-explaining it.

## The phases

1. **[Why Accessibility Isn't Optional](01-why-accessibility-isnt-optional.md)** - who actually needs
   this, the four WCAG pillars in plain language, and why semantic HTML already does most of the work.
2. **[ARIA, Focus Management, and Keyboard Navigation](02-aria-focus-management-and-keyboard-navigation.md)**
   - the first rule of ARIA, `tabindex`, visible focus styles, and trapping focus in a modal.
3. **[Testing with a Screen Reader and Automated Tools](03-testing-with-a-screen-reader-and-automated-tools.md)**
   - turning on VoiceOver or NVDA and tabbing through a real page, plus what Lighthouse and axe can and
   can't catch.

By the end, you'll know which HTML choices buy accessibility automatically, when ARIA is the right tool
versus a crutch for bad markup, and how to verify a page actually works for someone who isn't using a
mouse or a monitor the way you are.


---

# Why Accessibility Isn't Optional

Someone using a screen reader doesn't see your page - they hear it, one element at a time, navigating
by jumping between headings, links, and form fields. Someone with a motor impairment doesn't use a
mouse - every interaction goes through a keyboard: Tab to move forward, Shift+Tab to move back, Enter
or Space to activate. Someone with low vision has the page zoomed to 200% or runs a high-contrast mode
that strips your color palette down to black and white. These aren't three rare people. They're a
regular slice of anyone's user base, and building for them is not a separate project bolted onto
"real" development - it's a property of the same HTML and CSS you're already writing.

The business case is straightforward even if you set empathy aside: in the US, the ADA has been
applied to websites in thousands of lawsuits; the EU's Accessibility Act sets similar requirements. But
the mechanical case is more useful day to day - accessible markup is more robust markup. A page that
works without a mouse also works when a mouse driver glitches. A page with real heading structure is
also easier for you to navigate in devtools six months from now.

## The four pillars: POUR

WCAG (Web Content Accessibility Guidelines) organizes every requirement under four principles, nicknamed
POUR:

- **Perceivable** - users must be able to perceive the content through some sense. Text needs enough
  color contrast to read; images need alt text for people who can't see them; video needs captions for
  people who can't hear it.
- **Operable** - users must be able to operate the interface. Every interactive element needs to work
  by keyboard, not just mouse click or touch. Nothing should require a hover gesture with no keyboard
  equivalent.
- **Understandable** - content and interface behavior must be predictable. Form errors need to say what
  went wrong and where. Navigation shouldn't rearrange itself between pages for no reason.
- **Robust** - content must work across browsers, devices, and assistive technology, including tools
  that don't exist yet. This is why standard HTML elements matter more than custom ones: a `<button>`
  works in every screen reader ever built and every one that gets built next year.

Every accessibility rule you'll run into traces back to one of these four. "Add alt text" is
Perceivable. "Make the dropdown keyboard-operable" is Operable. "Don't rely on color alone to show an
error" is Understandable. "Use real HTML elements instead of divs" is Robust.

## Semantic HTML already does the work

The fastest way to fail at accessibility is to rebuild native browser behavior from scratch. The fastest
way to succeed is to not do that. Compare these two buttons:

```html
<button onclick="submitForm()">Submit</button>

<div onclick="submitForm()">Submit</div>
```

The `<button>` is focusable by pressing Tab, activates on both Enter and Space, gets announced by a
screen reader as "Submit, button," and shows a visible focus outline automatically. The `<div>` does
none of that. It's invisible to Tab, silent to a screen reader (announced as nothing, or at best as
plain text with no indication it's clickable), and gives keyboard users no way to activate it at all.
To make the `<div>` match the `<button>`, you'd need to add `tabindex="0"`, a `role="button"`, a
`keydown` handler for both Enter and Space, and manual focus-visible styling - four extra pieces of
code to reinvent something the browser already gives you.

This pattern repeats across HTML:

- `<nav>`, `<main>`, `<header>`, `<footer>` let screen reader users jump straight to a page region
  instead of listening to the whole thing top to bottom.
- A real `<label for="email">` tied to `<input id="email">` means clicking the label focuses the input,
  and a screen reader announces the field's purpose automatically - covered in
  [Forms That Work](/guides/forms-that-work).
- Heading tags (`<h1>` through `<h6>`) in order let a screen reader user pull up a list of headings and
  skip straight to the section they want, the same way a sighted user scans a page visually.
- `<a href>` is keyboard-focusable and announced as a link out of the box; a `<span>` styled to look
  like one is not.

None of this is exotic knowledge - it's the same HTML from
[HTML From Zero](/guides/html-from-zero), used as intended instead of flattened into `<div>`s with
click handlers. The lesson for the rest of this guide: reach for ARIA and custom JavaScript only for
what native HTML genuinely can't do - things like a tab panel, a modal dialog, or a combobox that lack
a direct HTML equivalent. Phase 2 covers exactly that gap.

Quick check before moving on:

```quiz
[
  {
    "q": "Which group does accessible design serve?",
    "choices": ["Only users with permanent disabilities", "A broad range of users, including keyboard-only, low-vision, and screen reader users", "Only users required to comply by law"],
    "answer": 1,
    "explain": "Screen reader users, keyboard-only users, low-vision users, and more are a routine part of any real audience, not an edge case."
  },
  {
    "q": "Which WCAG pillar covers 'every interactive element must work by keyboard'?",
    "choices": ["Perceivable", "Operable", "Robust"],
    "answer": 1,
    "explain": "Operable means users can interact with the page using something other than a mouse - keyboard, switch device, voice control."
  },
  {
    "q": "Why does <button onclick=...> beat <div onclick=...> for accessibility?",
    "choices": ["It looks different in the browser's default styling", "It's keyboard-focusable, activates on Enter/Space, and is announced correctly by screen readers automatically", "Divs cannot run JavaScript"],
    "answer": 1,
    "explain": "A native button gets focus, keyboard activation, and correct screen reader announcement for free. A div needs tabindex, a role, and manual key handling to match it."
  }
]
```


---

# ARIA, Focus Management, and Keyboard Navigation

ARIA (Accessible Rich Internet Applications) is a set of HTML attributes that describe roles, states,
and properties to assistive technology - things like `role`, `aria-label`, and `aria-expanded`. It
exists for one reason: some UI patterns have no native HTML equivalent. A tab panel, a modal dialog, a
combobox, a live chat notification - HTML has no `<tab-panel>` tag, so ARIA fills in what a screen
reader needs to announce.

## The first rule of ARIA

**No ARIA is better than bad ARIA.** If a native element already does what you need, use it instead of
faking it with ARIA on a `<div>`. This isn't a style preference - a native `<button>` gets keyboard
support, focus handling, and correct announcement built into the browser. Recreate it with
`<div role="button">`, and you've taken on the job of manually adding everything the browser used to
do for you: a `tabindex`, a `keydown` handler for Enter and Space, and focus styling. Miss any one of
those and you've shipped something that announces as a button but doesn't act like one - arguably worse
than a plain unstyled `<div>`, because a screen reader user now expects button behavior and doesn't get
it.

Before reaching for `role="button"`, `role="link"`, or `role="checkbox"`, check whether `<button>`,
`<a>`, or `<input type="checkbox">` already does the job. It almost always does. Save ARIA for what HTML
genuinely lacks.

## tabindex: 0, -1, and why positive numbers are a trap

`tabindex` controls whether an element can receive keyboard focus and where it sits in the tab order.

- **`tabindex="0"`** - makes an element focusable in the natural DOM order, wherever it sits in the
  document. Use this on a custom interactive element (like a `<div>` acting as a tab, if you truly can't
  use a native element) so it joins the tab sequence like everything else.
- **`tabindex="-1"`** - removes an element from the natural Tab order but leaves it focusable
  programmatically, via `element.focus()` in JavaScript. Use this for things like a modal's heading,
  which you want to move focus to when it opens, but which no one should reach by tabbing.
- **Positive values (`tabindex="1"`, `"2"`, etc.)** - force a specific tab order, overriding the DOM.
  Avoid these. They create a second, invisible ordering that has to be kept in sync by hand every time
  the page changes, and one forgotten update produces a tab order that jumps around unpredictably. If
  elements tab in the wrong order, the fix is almost always to move them in the HTML, not to patch the
  order with numbers.

```html
<!-- Focusable in natural order -->
<div tabindex="0" role="tab">Settings</div>

<!-- Focusable only via JS, e.g. modal.focus() on open -->
<h2 tabindex="-1" id="modal-title">Confirm deletion</h2>
```

## Visible focus styles are not optional

Every browser ships a default focus outline - that blue (or platform-specific) ring around whatever
element currently has keyboard focus. It's the only way a keyboard user tracks where they are on the
page. Removing it without a replacement is one of the most common accessibility regressions:

```css
/* Don't ship this */
:focus {
  outline: none;
}
```

That single rule makes a page silently unusable by keyboard: buttons still work when Tabbed to and
activated, but the user has no visual signal of which one is about to fire. If the default outline
clashes with a design, replace it - don't delete it:

```css
:focus-visible {
  outline: 2px solid #2563eb;
  outline-offset: 2px;
}
```

`:focus-visible` (rather than `:focus`) matters here too - it shows the ring for keyboard navigation but
skips it for mouse clicks, which is why modern sites don't flash an outline every time you click a
button with a mouse.

## Trapping focus in a modal

Open a modal dialog and Tab should cycle through the elements inside it - not escape into the page
behind it. Without this, a keyboard user can tab straight past the dialog into background content they
can't even see, because the modal is covering it visually while the DOM tab order ignores that.

```html
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-title">
  <h2 id="modal-title" tabindex="-1">Delete this file?</h2>
  <button id="cancel-btn">Cancel</button>
  <button id="confirm-btn">Delete</button>
</div>
```

```js
const modal = document.querySelector('.modal');
const focusable = modal.querySelectorAll('button, [href], input, [tabindex]:not([tabindex="-1"])');
const first = focusable[0];
const last = focusable[focusable.length - 1];

document.getElementById('modal-title').focus(); // move focus in on open

modal.addEventListener('keydown', (e) => {
  if (e.key === 'Escape') closeModal();

  if (e.key !== 'Tab') return;

  if (e.shiftKey && document.activeElement === first) {
    e.preventDefault();
    last.focus();
  } else if (!e.shiftKey && document.activeElement === last) {
    e.preventDefault();
    first.focus();
  }
});
```

Three things happen here: focus moves into the modal the moment it opens (so a screen reader announces
the dialog immediately), Tab and Shift+Tab wrap around inside the modal instead of leaking out, and
Escape closes it - a keyboard user's expected shortcut for dismissing any dialog. When the modal closes,
return focus to whatever element opened it, so the user picks up where they left off instead of landing
back at the top of the page.

`aria-modal="true"` combined with `role="dialog"` tells assistive technology to treat everything outside
the modal as inert while it's open - screen readers won't navigate into background content, matching
what the focus trap does for keyboard users.

Try it yourself:

```quiz
[
  {
    "q": "What's the first rule of ARIA?",
    "choices": ["Add ARIA to every interactive element for safety", "No ARIA is better than bad ARIA - use a native element instead when one exists", "ARIA should replace all semantic HTML"],
    "answer": 1,
    "explain": "A native element like <button> already handles focus, keyboard activation, and correct announcement. Recreating it with ARIA on a div means manually replicating all of that correctly."
  },
  {
    "q": "Why should you avoid positive tabindex values (1, 2, 3...)?",
    "choices": ["They are not supported in most browsers", "They create a separate manual tab order that has to be kept in sync by hand and easily breaks", "They make elements unfocusable"],
    "answer": 1,
    "explain": "Positive tabindex overrides the natural DOM order with numbers you must maintain yourself - one missed update produces an unpredictable tab sequence."
  },
  {
    "q": "What's wrong with `:focus { outline: none; }` and no replacement?",
    "choices": ["It slows down page rendering", "It removes the only visual cue keyboard users have for where focus currently is", "It breaks screen readers"],
    "answer": 1,
    "explain": "The focus outline is how a keyboard-only user tracks their position on the page. Removing it without a visible replacement makes the page unusable without a mouse."
  }
]
```


---

# Testing with a Screen Reader and Automated Tools

Reading about accessibility only goes so far. The fastest way to understand what's broken is to close
your eyes, or at least your assumptions about a mouse, and use a screen reader on your own page for five
minutes.

## VoiceOver on Mac

VoiceOver ships with every Mac. Turn it on with `Cmd+F5` (or ask Siri: "turn on VoiceOver"). It starts
narrating immediately - the interface, then whatever's under your cursor.

Basic navigation once it's on:

- **Tab** - move to the next focusable element (buttons, links, form fields).
- **VO+Right Arrow / VO+Left Arrow** (VO = Control+Option, held together) - move to the next or previous
  item, focusable or not, reading everything on the page in order.
- **VO+Space** - activate the current item, like a click.
- **VO+U** - open the rotor, a menu that lists all headings, links, or form controls on the page so you
  can jump directly to one.
- **Cmd+F5** - turn VoiceOver back off.

Load a real page you've built, turn on VoiceOver, and Tab through it. Listen for: buttons announced
as "button" (not silence, not just the label with no role), images with meaningful alt text instead of
"image" or the filename, and form fields that announce their label, not just "edit text."

## NVDA on Windows

NVDA is a free, open-source screen reader - download it from nvaccess.org. Launch it and it starts
narrating right away.

Basic navigation:

- **Tab / Shift+Tab** - move forward/backward through focusable elements.
- **Down Arrow** (in browse mode, NVDA's default for reading web pages) - move to the next line or
  element, read aloud.
- **H** - jump to the next heading; **1** through **6** jump to a heading of that specific level.
- **F** - jump to the next form field.
- **Insert+Q** - quit NVDA.

Same exercise: Tab through your page and listen. A well-marked-up page announces its structure without
you doing anything special - that's the semantic HTML from Phase 1 paying off. A `<div>`-and-`<span>`
page announces nothing useful: no roles, no labels, just streams of plain text.

## What to listen for

- Every interactive element announces a role ("button," "link," "checkbox, not checked") - not silence.
- Every image either has descriptive alt text or, if purely decorative, an empty `alt=""` so the screen
  reader skips it instead of reading a useless filename.
- Every form field announces its label before its type ("Email, edit text," not just "edit text").
- Headings are in a sensible order (`h1` then `h2` then `h3`, not jumping from `h1` to `h4`), because
  that's what a screen reader user's heading-jump list is built from.

## Automated tools: fast, but partial

**Lighthouse** ships in Chrome DevTools (Lighthouse tab → run an audit → check "Accessibility"). **axe
DevTools** is a free browser extension from Deque that does a similar scan with often more detail per
issue. Both crawl the DOM and flag things like missing alt text, insufficient color contrast, form
inputs without labels, and invalid ARIA usage. Run one before every release - they take seconds and
catch real, common mistakes.

Their real limit: automated accessibility testing catches roughly a **third** of WCAG issues. A tool can
check whether an image has an `alt` attribute; it cannot judge whether the alt text actually describes
the image, or whether a modal's focus trap actually works, or whether a custom dropdown responds
correctly to arrow keys. Those need a human - ideally with a keyboard and a screen reader, doing exactly
what Phases 2 and 3 walked through.

Treat automated tools as a floor, not a finish line: run Lighthouse or axe on every page as a fast
regression check, then manually tab through and listen to anything genuinely interactive - forms,
modals, custom widgets, navigation menus. That combination catches what either approach misses alone.

Last check for this guide:

```quiz
[
  {
    "q": "What keyboard shortcut turns VoiceOver on/off on a Mac?",
    "choices": ["Cmd+F5", "Ctrl+Alt+V", "Cmd+Shift+A"],
    "answer": 0,
    "explain": "Cmd+F5 toggles VoiceOver on macOS. NVDA on Windows is a separate download from nvaccess.org."
  },
  {
    "q": "Roughly what proportion of accessibility issues do automated tools like Lighthouse and axe catch?",
    "choices": ["Nearly all of them", "About a third", "None - they only check performance"],
    "answer": 1,
    "explain": "Automated scanners catch structural issues (missing alt, bad contrast, missing labels) but can't judge whether alt text is meaningful or whether a focus trap actually works - roughly a third of real issues, by common estimates."
  },
  {
    "q": "Why still test manually with a screen reader if you already ran Lighthouse?",
    "choices": ["Lighthouse doesn't run in Chrome", "Lighthouse can't judge things like whether alt text is meaningful or a custom widget behaves correctly with a keyboard", "Manual testing is faster than automated testing"],
    "answer": 1,
    "explain": "Automated tools check for the presence of attributes, not their quality or actual keyboard/screen-reader behavior. Interactive widgets especially need a human pass."
  }
]
```

Accessibility habits carry into the next layer of the stack, too - where you store data affects who can
use your site. See [Browser Storage and Cookies](/guides/browser-storage-and-cookies) for how
`localStorage`, `sessionStorage`, and cookies work, including the consent and privacy angles that come
with them.
