Skip to content
Episode 18 · The Book button that said nothing

Accessibility as an engineering requirement

Tuesday, 10:15: Sara, a blind guest, writes in. Her screen reader reads the hotel page fine, but the "Book" button is silent, so she can't finish a booking. An automated scan finds 41 issues in our 5-step booking flow.

Some of your users can't see the screen or use a mouse. Here's how they use your app, and why plain, correct HTML does most of the work for them.

30 min intermediateSemantic HTML as the accessibility foundationKeyboard operability and focus managementUsing ARIA sparingly and correctlyTesting accessibility (automated + manual)What the frontend is responsible for
  1. Problem

    A blind guest using a screen reader, a guest with a tremor using only a keyboard, and a guest with low vision on a sunny terrace all try to book a room on your site.

  2. Why the simple system fails

    A booking flow built from clickable divs, placeholder-only inputs, colour-only error messages and a custom date picker that traps focus is impossible for them to complete, and in many markets that's also a legal risk.

  3. Engineering concept

    Expose meaning (role, name, state) through semantic HTML, make every action keyboard-operable with visible focus, ensure perceivable content (text alternatives, contrast, not colour alone), and use ARIA only to fill gaps.

  4. Technology

    Semantic elements, labels and fieldsets, the accessibility tree, focus management, ARIA roles/states, WCAG AA criteria, axe/Lighthouse in CI, manual keyboard and screen-reader checks.

  5. Trade-offs

    Custom widgets give design freedom but cost real engineering to make accessible; native elements are accessible by default but harder to style. Automated tests are cheap but catch only part of the issues; manual testing finds the rest.

01

The problem

2 min
Tuesday, 10:15. A support email from Sara, a blind guest.
  1. Maya · Product manager
    Sara uses a screen reader. She hears the hotel page fine, but "Book" says nothing, and she can't finish a booking.
  2. You · Engineer at StayHub
    On screen it's a big blue Book button. What does her screen reader see?
  3. Lina · Senior engineer, your mentor
    Last episode of the module: you lead this one. I'll check.
02

The big idea

4 min
What Sara's screen reader hears
The big idea

Most accessibility is engineering: semantic HTML gives role, name and keyboard for free.

Use the element that means what you want, and the browser does the rest. A <button> fixes Sara's booking in one line.

03

How it works

5 min
How Sara uses the page
  1. 1
    DOM

    Our HTML becomes the DOM, as in the first episode of this module.

  2. 2
    Accessibility tree

    The browser builds the : for each element, a role (button, link), a name ("Book Deluxe Double") and a state (pressed, invalid).

  3. 3
    Screen reader

    A speaks that tree: "Book Deluxe Double, button".

  4. 4
    Keyboard

    Sara moves with Tab, jumps by headings, presses Enter or Space. Only focusable elements are reachable.

  5. 5
    Our div

    No role, not focusable: Tab skips it, and at best it's read as plain text. Dead end.

So the work is engineering: 1. : <button> for actions, <a href> for navigation, <label> for inputs. 2. Keyboard: everything clickable works with Tab, Enter, Space, with a visible . 3. Perceivable: alt text, enough (our grey "from €140" is 2.8:1; AA asks 4.5:1), never colour alone. 4. sparingly: attributes that change what's announced, never behaviour.

04

Watch a decision being made

4 min
Worked example

You lead the booking-flow fix, Lina checks

Sara can't book, and a scan finds 41 issues in the 5-step flow. Make it usable by keyboard and screen reader, and keep it that way.

  1. 1Scan, then unplug the mouse

    axe: 41 issues, four patterns (32 clickable divs, 4 unlabelled inputs, 3 low contrast, 2 missing alt). Then book with keys only: step 3, the custom date grid, is a dead end.

    Why: Lina's check: "Right. Scanners can't judge focus or dead ends; a real keyboard booking can."

  2. 2Fix the components, not the pages

    Clickable divs become <button> and <a href> in the shared card and button components; every input gets a visible <label>.

    Why: Lina's check: "One fixed component fixes every page that uses it."

  3. 3Rebuild the date picker on a native dialog

    <dialog> with showModal(), days as buttons with arrow keys, focus returned to the opener on close.

    tsx
    opener.current = document.activeElement as HTMLElement;
    dialog.showModal();        // focus moves in, page behind is inert, Esc closes
    // on close:
    opener.current?.focus();   // back to "Choose dates"

    Why: Lina's check: "The native dialog traps focus and handles Escape. You only add what's missing."

  4. 4Fix contrast, lock it in

    Price text to at least 4.5:1, "Error:" next to red borders. axe in CI, tests that find buttons by role and name, "book by keyboard" in the definition of done. Then ask Sara to try again.

    Why: Lina's check: "If a test can't find the button by role and name, neither can a screen reader."

The whole thing in one line: Scan for patterns, unplug the mouse for dead ends, fix shared components natively, lock it in with CI and a keyboard check.
05

In real life

3 min
Two well-known services, two routes

GOV.UK: native HTML first

  • Publishes an open design system built on plain, semantic HTML
  • Has publicly committed to accessibility standards across its services
  • Design freedom is deliberately limited

Fits services everyone must use, with forms at their core.

Google Docs: custom rendering, a screen-reader mode

  • Its editor draws much of the document itself
  • Its help pages tell screen-reader users to turn on a dedicated setting

Fits a complex editor native elements can't express.

06

What goes wrong

3 min
`outline: none` on focus

Keyboard users lose track of where they are.

Instead

Keep a visible style; use :focus-visible.

Placeholder as label

It disappears on typing and isn't reliably announced as the name.

Instead

A visible <label> tied to the input.

Treating a green score as done

Automated tools catch only part of real issues, often cited as a third to a half.

Instead

A keyboard booking and a screen-reader check in the definition of done.

07

Trade-offs

3 min
OptionFits whenYou payMain risk
Native HTMLAlmost alwaysSome controls hard to styleLittle
Custom widget + ARIANo native element fitsAll keyboard, focus, announcementsSubtly broken for screen readers
Headless libraryComplex widgets, several teamsA dependency, upgradesWrong labels in use
Friday, 10:00. The date picker needs rebuilding.
  1. Maya · Product manager
    I want our own date picker with the price under each day. Our best feature for flexible guests.
  2. Karim · CTO
    Native <input type="date"> is accessible for free and ships this week. Custom is weeks of work.
  3. Maya · Product manager
    The native one can't show prices. Guests book blind on cost.
08

Your job as tech lead

4 min

Claude Code can write the code for this. You still own the decisions, and you have to know what good looks like to accept or reject its work.

Decisions you own
  • The standard you commit to (WCAG AA for the booking flow), and that a release can't ship if a keyboard user can't book.
  • Who tests what: axe in CI as a safety net, a human keyboard run through all 5 steps every release, a regular screen-reader check.
  • Which colours change: grey price text at 2.8:1 must reach 4.5:1, with design's sign-off.
Tell Claude Code
Sara, a blind guest with a screen reader, couldn't book: "Book" is a clickable div read as plain text. axe finds 41 issues in our 5-step booking flow, and a keyboard-only test gets stuck at step 3, the date picker. Replace clickable divs with real buttons and links, name every icon button, label every field, and make the date picker keyboard-usable. Darken "from €140" to at least 4.5:1. Keep the steps. Plan first, add axe checks, and describe your keyboard check of each step.
Check before you accept
  • Book a room yourself with Tab, Enter, Space and arrows, always seeing where focus is.
  • Claude lists each div it replaced and with what element. "Added role=button" is not a fix.
  • One screen-reader pass: each button says what it does, like "Book Hotel Alfama, button".
Red flags
  • role="button" on divs without focus or Enter and Space.
  • The focus outline removed because it "looks ugly".
  • "Accessible" because axe reports zero, with no keyboard test.
09

Check yourself

2 min
Q1/3Debugging
A keyboard-only tester can't press "Book now". The markup is <div class="btn-primary" onClick={book}>Book now</div>. What's the best fix?
How confident are you?

Next episode: the mobile app's hotel screen makes 6 API calls in a row, and Maya wants "rooms left" to update live. Is our REST API still the right shape?

Finished?

Mark it complete to update your skill map.

Go deeper (optional)You don't need this to understand the lesson. Open it when a project needs the details: Technical deep dive, Visual diagram, Analogy, Check your understanding, Practice ladder, Explain it yourself.
10

Technical deep dive

Booking with the mouse unplugged
Worked example

You lead the booking-flow fix, Lina checks

Sara can't book with a screen reader, and an automated scan finds 41 issues in the 5-step booking flow. Make the flow usable by keyboard and screen reader, and stop it from breaking again.

  1. 1Scan for the repeated patterns

    Run axe on each step. 41 issues, but only four patterns: 32 clickable divs, 4 inputs without labels, 3 low-contrast texts, 2 images without alt.

    Why: Grouping issues by pattern shows that a few component fixes clear most of the list.

  2. 2Unplug the mouse and book

    Try to book with only Tab, Shift+Tab, Enter, Space, arrows and Escape. Steps 1–2 pass. Step 3, the custom date grid, is a dead end: focus can't enter it, and it escapes behind the modal.

    Why: Scanners can't judge focus order or dead ends. A real booking by keyboard can.

  3. 3Fix the components, not the pages

    Replace clickable divs with <button> and <a href> in the shared card and button components; give every input a visible <label>.

    html
    <label for="email">Email</label>
    <input id="email" type="email" autocomplete="email"
      aria-invalid="true" aria-describedby="email-err">
    <p id="email-err">Error: enter an email like name@example.com</p>

    Why: One fixed component fixes every page that uses it. Fixing pages one by one leaves the next page broken.

  4. 4Rebuild the date picker on a native dialog

    Use <dialog> with showModal(), days as <button>s with arrow-key movement, and return focus to the opener on close.

    tsx
    opener.current = document.activeElement as HTMLElement;
    dialog.showModal();        // focus moves in, page behind is inert, Esc closes
    // on close:
    opener.current?.focus();   // back to "Choose dates"

    Why: The native dialog handles focus trapping and Escape. We only add what's missing: arrow keys and giving focus back.

  5. 5Fix what colour alone says

    Darken the price text to reach at least 4.5:1, and add the word "Error:" to messages that were only a red border.

    Why: Low-vision and colour-blind guests miss grey-on-white and red-only signals.

  6. 6Lock it in

    Add axe to component tests and CI, query elements by role and name in tests, and add "book by keyboard" to the definition of done. Then ask Sara to try again.

    ts
    expect(screen.getByRole("button", { name: /book deluxe double/i })).toBeEnabled();
    expect(await axe(container)).toHaveNoViolations();

    Why: If a test can't find the button by role and name, neither can a screen reader. CI stops regressions; a real user confirms the fix.

The whole thing in one line: You grouped 41 issues into four patterns, found the keyboard dead end the scan missed, fixed shared components and the date picker natively, fixed contrast and colour-only errors, and locked it in with tests and a keyboard check.
NeedUse first (native)ARIA only if…
Clickable action<button>Never needed for a plain action
Navigation<a href>Never needed for a plain link
Labelled input<label for> + <input>aria-label when no visible label is possible (icon-only)
Live status ("Room held for 10 min")Visible textaria-live="polite" so updates are announced
Tabs, comboboxNo full native equivalentFollow the ARIA Authoring Practices pattern, including keyboard
ARIA fills gaps. It doesn't add behaviour: you still write the keyboard support.
11

Visual diagram

Click a component

From your markup to what assistive technology announces.

12

Analogy

  • In a buildingStandard signs for lifts and exitsOn a web pageSemantic HTML
  • In a buildingA door painted to look like a wallOn a web pageA clickable div
  • In a buildingThe floor plan read aloud at receptionOn a web pageThe accessibility tree
  • In a buildingAn "Exit" sticker on a cupboard doorOn a web pageWrong ARIA
  • In a buildingA ramp next to the stairsOn a web pageKeyboard support

Where the analogy breaks: a building is inspected once. A web page changes every week, so accessibility checks belong in your automatic tests, plus regular hands-on keyboard and screen-reader checks.

13

Check your understanding

Q1/3Scenario
The price "from €140" is light grey (#999) on white, 16px regular. The contrast ratio is about 2.8:1. Against WCAG AA, what's the verdict?
How confident are you?
14

Practice ladder

Step 1 · Trace

Tab through a room card

Scenario Warm-up

A guest using only the keyboard presses Tab repeatedly through this room card. Focus arrives from the element just before it.

<div class="card" onclick="openRoom(42)">
  <img src="room.avif">
  <h3>Deluxe Double</h3>
  <span class="price" style="color:#999">from €140</span>
  <div class="heart" onclick="fav(42)">♥</div>
  <a href="/rooms/42/photos">See photos</a>
  <button type="button">Book</button>
</div>

List, in order, which elements receive focus, what a screen reader announces for each, and which actions the guest can't reach at all.

Step 2 · Complete

Finish the accessible booking field

Code Practice

Here's the guest-name field at checkout, after a first attempt. Fill in the three gaps marked ___ so that the field has a real name, the error is announced and linked to the field, and the error isn't conveyed by colour alone.

___                                         <!-- 1: the field's name -->
<input id="guest-name" type="text" autocomplete="name"
  aria-invalid="true" ___>                  <!-- 2: link the error -->
<p id="name-err" class="error">___</p>      <!-- 3: the error text -->
Step 3 · Solve

The room-held countdown

Debugging Practice

At checkout, StayHub holds the room for 10 minutes and shows a countdown at the top: "Room held for 9:59". At 0:00 the hold is released and a red banner appears: "Room released".

A keyboard-only guest reports two problems: 1. Her screen reader never told her the hold was about to end, so she lost the room. 2. When the "Room released" dialog opened, focus stayed on the form behind it and she kept typing into fields she couldn't see.

Propose fixes for both, and say how you'd test them.

Step 4 · Decide

Accessibility check: before release or after?

Decision Stretch

Karim: "Six engineers, lots to ship. Do we add accessibility checks to every pull request, or do a full audit twice a year?"

The facts: - The last audit found 41 issues; fixing them took about 2 weeks. - 80% of issues came from 5 shared components (button, card, input, dialog, date picker). - Adding axe to component tests and CI takes about 2 days; a keyboard check per feature adds about 15 minutes. - An external audit costs about two weeks of one engineer's time to support and fix.

Write a short decision: "We chose X over Y because…, accepting the risk of…".

15

Explain it yourself

A teammate says accessibility is a design concern and can be handled after launch. Explain why it's an engineering requirement and what you'd put in place.

0 words