EthicsComputerCourses
Join training +91 90532 10052

Project: Audit & Fix a Page

Web Accessibility · Lesson 28 of 28 Advanced 8 min read Updated

In this project you put the whole course to work. You will take a small but badly built appointment page for a clinic in Lucknow, audit it the way a professional accessibility tester does, record each problem against a WCAG 2.2 success criterion, and then rebuild the page so it meets Level AA.

The page is short on purpose, but every problem in it is one you will meet on real websites: fake buttons, missing labels, a pop-up that traps nobody but loses everyone, low contrast and silent updates. Work through the steps in order and test after each one.

The page to audit

Run the starting page. It looks reasonable with a mouse. Try it with only the keyboard, then read on.

HTMLTry it yourself
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
  <title>Clinic</title>
  <style>
    body { font-family: sans-serif; margin: 0; }
    .top { background: #7fc4a0; color: #fff; padding: 12px; font-size: 26px; font-weight: bold; }
    .menu span { margin-right: 14px; cursor: pointer; text-decoration: underline; }
    .box { width: 700px; padding: 12px; }
    .hint { color: #b3b3b3; }
    input, select { border: 1px solid #e0e0e0; padding: 6px; }
    .btn { background: #7fc4a0; color: #fff; padding: 8px 16px; display: inline-block; cursor: pointer; }
    *:focus { outline: none; }
    .popup { display: none; position: fixed; top: 30%; left: 30%; background: #fff; border: 1px solid #999; padding: 20px; }
    .err { border-color: red; }
  </style>
</head>
<body>
  <div class="top">Gomti Nagar Family Clinic</div>
  <div class="menu"><span onclick="location.hash='doctors'">Doctors</span><span onclick="location.hash='fees'">Fees</span></div>
  <div class="box">
    <div style="font-size: 22px; font-weight: bold">Book an appointment</div>
    <img src="https://course.ethicscomputer.in/assets/samples/avatar.svg" width="80" height="80">
    <p class="hint">Fields in red are compulsory.</p>
    <input id="n" placeholder="Patient name"><br><br>
    <input id="m" placeholder="Mobile"><br><br>
    <select id="d"><option>Dr. Meera Iyer (General)</option><option>Dr. Farhan Ali (Child specialist)</option></select><br><br>
    <div class="btn" onclick="book()">Book</div>
    <p id="msg"></p>
  </div>
  <div class="popup" id="pop">Appointment booked! <span onclick="document.getElementById('pop').style.display='none'">X</span></div>
  <script>
    function book() {
      const n = document.getElementById("n");
      const m = document.getElementById("m");
      n.classList.toggle("err", n.value === "");
      m.classList.toggle("err", !/^\d{10}$/.test(m.value));
      if (n.value && /^\d{10}$/.test(m.value)) {
        document.getElementById("pop").style.display = "block";
      }
    }
  </script>
</body>
</html>

Step 1: automated scan

Start with a tool, because it finds the obvious problems in seconds. Open the page in Chrome, run Lighthouse's accessibility audit or the axe DevTools extension, or paste the axe snippet from Automated Tools & Their Limits at the end of the body.

A typical automated run reports:

  • The <html> element has no lang attribute.
  • The image has no alt attribute.
  • Text contrast is too low: white on #7fc4a0 is about 2.04:1 and the grey hint #b3b3b3 on white is about 2.10:1.
  • The viewport meta tag limits zooming with maximum-scale=1.
  • The doctor <select> has no accessible name. The two text fields pass, because the tool accepts their placeholders as names, even though those names disappear when typing starts.

Write these down, but do not stop here. Remember that tools see only part of the picture.

Step 2: keyboard test

Put the mouse aside and press Tab from the top of the page.

  • Focus reaches the name field, the mobile field and the select, and then leaves the page. The "Doctors" and "Fees" menu items, the "Book" control and the "X" close control are all <span> or <div> elements, so they cannot be reached or used at all. A keyboard user cannot book an appointment.
  • You cannot see where focus is, because *:focus { outline: none; } removes every focus indicator.
  • If you trigger the pop-up with the mouse, focus stays behind it, Esc does nothing, and the close "X" is unreachable.

Step 3: screen reader and structure check

Start NVDA (or VoiceOver or TalkBack) and follow the ten-minute script from Testing with Screen Readers.

  • The title "Clinic" does not describe the page.
  • The headings list is empty: the clinic name and "Book an appointment" are styled <div> elements.
  • There are no landmarks: no <header>, <nav> or <main>.
  • The fields are announced only by their placeholders, which disappear when typing starts. The select has no label at all, so users hear only the selected doctor's name.
  • When booking fails, the borders turn red, but nothing is announced, and the message "Fields in red are compulsory" depends on colour.
  • When booking succeeds, the pop-up appears silently.

Step 4: visual checks

  • Contrast: the header text, the hint and the "Book" button text all fail 4.5:1. The input border #e0e0e0 is about 1.32:1 against white, far below the 3:1 needed for 1.4.11.
  • Reflow: the box is fixed at width: 700px, so at 320 CSS pixels the page scrolls sideways.
  • Zoom: the viewport tag tries to block pinch zoom on phones.
  • Targets: the "X" close control is a single small character with no padding.

Step 5: write the audit report

A good report lists each issue once, with the WCAG criterion, its level, how serious it is for users, and a fix. Here is the report for this page:

#IssueWCAG 2.2 criterionLevelFix
1No page language3.1.1 Language of PageA<html lang="en">
2Vague page title2.4.2 Page TitledA"Book an appointment - Gomti Nagar Family Clinic"
3Image without alt1.1.1 Non-text ContentAalt describing the doctor photo, or alt="" if decorative
4Fake headings, no landmarks1.3.1 Info and RelationshipsA<h1>, <h2>, <header>, <nav>, <main>
5Menu, Book and close controls not keyboard operable2.1.1 Keyboard; 4.1.2 Name, Role, ValueALinks for menu, <button> for actions
6Focus indicator removed2.4.7 Focus VisibleAARemove the rule; add a :focus-visible style
7Fields without labels3.3.2 Labels or Instructions; 1.3.1AVisible <label> for every field
8Errors shown by red border only3.3.1 Error Identification; 1.4.1 Use of ColorAText messages, aria-invalid, error summary
9Success not announced; pop-up not accessible4.1.3 Status Messages; 2.1.1AA / AStatus message, or a real <dialog>
10Low text contrast1.4.3 Contrast (Minimum)AADarker colours, at least 4.5:1
11Low input border contrast1.4.11 Non-text ContrastAABorder of at least 3:1
12Fixed 700px width1.4.10 ReflowAAmax-width and flexible layout
13Zoom limited1.4.4 Resize TextAAStandard viewport tag
14Tiny close target2.5.8 Target Size (Minimum)AAReal button with at least 24 by 24 pixels, ideally 44
15No autocomplete on name and mobile1.3.5 Identify Input PurposeAAautocomplete="name" and "tel-national"

Order your fixes by impact: issues that block a task (5, 7, 9) come first, then serious ones (3, 4, 6, 8, 10), then the rest.

Step 6: the fixed page

Here is the rebuilt page. Each fix uses the simplest native HTML that works, with a small amount of script.

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Book an appointment - Gomti Nagar Family Clinic</title>
  <style>
    body { font: 1rem/1.5 system-ui, sans-serif; margin: 0; color: #333333; }
    header { background: #1b6e20; color: #fff; padding: 0.75rem 1rem; }
    header p { margin: 0; font-size: 1.5rem; font-weight: bold; }
    nav a { color: #fff; margin-right: 1rem; }
    main { max-width: 40rem; padding: 1rem; }
    label { display: block; font-weight: 600; margin-top: 0.9rem; }
    .hint { color: #595959; }
    input, select { font: inherit; padding: 0.4rem; border: 2px solid #767676; width: 100%; box-sizing: border-box; }
    .error { color: #b00020; display: block; }
    [aria-invalid="true"] { border-color: #b00020; border-width: 3px; }
    button { font: inherit; background: #1b6e20; color: #fff; border: 0; padding: 0.6rem 1.2rem; min-height: 44px; margin-top: 1rem; }
    :focus-visible { outline: 3px solid #0b5cad; outline-offset: 2px; }
    header :focus-visible { outline-color: #ffd166; }
    dialog { border: 0; border-radius: 10px; padding: 1.25rem; }
    dialog::backdrop { background: rgb(0 0 0 / 0.5); }
  </style>
</head>
<body>
  <header>
    <p>Gomti Nagar Family Clinic</p>
    <nav aria-label="Main"><a href="#doctors">Doctors</a><a href="#fees">Fees</a></nav>
  </header>
  <main>
    <h1>Book an appointment</h1>
    <img src="https://course.ethicscomputer.in/assets/samples/avatar.svg" width="80" height="80" alt="">
    <p class="hint">All fields are required.</p>
    <div id="summary" tabindex="-1" role="group" aria-labelledby="summary-title" hidden>
      <h2 id="summary-title">There is a problem</h2>
      <ul id="summary-list"></ul>
    </div>
    <form id="booking" novalidate>
      <label for="name">Patient name</label>
      <span class="error" id="name-error"></span>
      <input id="name" name="name" autocomplete="name" aria-describedby="name-error">

      <label for="mobile">Mobile number</label>
      <span class="hint" id="mobile-hint">10 digits, for example 9876543210</span>
      <span class="error" id="mobile-error"></span>
      <input id="mobile" name="mobile" inputmode="numeric" autocomplete="tel-national" aria-describedby="mobile-hint mobile-error">

      <label for="doctor">Doctor</label>
      <select id="doctor" name="doctor">
        <option>Dr. Meera Iyer (General)</option>
        <option>Dr. Farhan Ali (Child specialist)</option>
      </select>

      <button type="submit">Book appointment</button>
    </form>
  </main>

  <dialog id="done" aria-labelledby="done-title">
    <h2 id="done-title">Appointment booked</h2>
    <p id="done-text"></p>
    <form method="dialog"><button autofocus>Close</button></form>
  </dialog>

  <script>
    const form = document.querySelector("#booking");
    const rules = [
      { id: "name", ok: (v) => v.trim() !== "", text: "Enter the patient's name" },
      { id: "mobile", ok: (v) => /^[6-9]\d{9}$/.test(v), text: "Enter a 10-digit mobile number, for example 9876543210" },
    ];
    form.addEventListener("submit", (event) => {
      event.preventDefault();
      const list = document.querySelector("#summary-list");
      list.replaceChildren();
      for (const rule of rules) {
        const field = document.getElementById(rule.id);
        const error = document.getElementById(rule.id + "-error");
        const ok = rule.ok(field.value);
        error.textContent = ok ? "" : "Error: " + rule.text;
        if (ok) {
          field.removeAttribute("aria-invalid");
        } else {
          field.setAttribute("aria-invalid", "true");
          const li = document.createElement("li");
          li.append(Object.assign(document.createElement("a"), { href: "#" + rule.id, textContent: rule.text }));
          list.append(li);
        }
      }
      const summary = document.querySelector("#summary");
      if (list.children.length) {
        summary.hidden = false;
        document.title = "Error: Book an appointment - Gomti Nagar Family Clinic";
        summary.focus();
        return;
      }
      summary.hidden = true;
      document.title = "Book an appointment - Gomti Nagar Family Clinic";
      const doctor = document.querySelector("#doctor").value;
      document.querySelector("#done-text").textContent =
        `${document.querySelector("#name").value} is booked with ${doctor}. We will send an SMS to ${document.querySelector("#mobile").value}.`;
      document.querySelector("#done").showModal();
    });
  </script>
</body>
</html>

What changed, and why

  • Structure: the page has a language, a descriptive title, a <header>, a labelled <nav>, a <main>, and real <h1> and <h2> headings. The photo turned out to be decorative (it shows no real doctor), so it gets alt="".
  • Keyboard: the menu items are links, and the action is a real submit <button>. Focus is visible everywhere through :focus-visible, with a yellow ring in the dark green header.
  • Forms: each field has a visible label; the mobile field has a format hint; autocomplete values help users fill the form. Errors are written in text, linked with aria-describedby and marked with aria-invalid, and an error summary receives focus and links to each field. The page title starts with "Error:" while errors exist.
  • Success: a native modal <dialog> replaces the fake pop-up. It is named by its heading, focus moves to its Close button, Esc closes it, and focus returns to the Book button.
  • Visual: all text now has at least 4.5:1 contrast (white on #1b6e20 is about 6.37:1), input borders are #767676 (about 4.54:1), the layout uses max-width in rem, the button is at least 44 pixels tall, and zoom is allowed.

Step 7: test again

Fixing is not finished until you re-test with the same methods:

  1. Run axe or Lighthouse again: there should be no failures.
  2. Keyboard: Tab through the menu, fields and button; submit with empty fields and follow the summary links; book successfully, press Esc in the dialog and check where focus returns.
  3. Screen reader: check the headings list, the landmarks, each field's label and hint, the error announcements and the dialog name.
  4. Zoom to 400% or set the window to 320 pixels wide: no sideways scrolling.
  5. Apply the WCAG text spacing values from Text Resize, Zoom & Reflow: nothing is cut off.

Then update the report: mark each issue fixed with the date, and note anything you could not fix along with a workaround. That list becomes the "Known limitations" section of your accessibility statement.

Key points

  • A real audit combines automated tools, keyboard testing, screen reader testing and visual checks.
  • Record each issue once, with the WCAG criterion, level, impact and a concrete fix.
  • Fix blockers first: controls that cannot be reached, fields with no labels, silent results.
  • Native HTML (<button>, <a href>, <label>, <dialog>, landmarks) fixes most problems with little code.
  • Always re-test with the same methods after fixing, and feed open issues into your accessibility statement.

Exercise

Extend the fixed page with a "Preferred date" field using <input type="date"> and a "Preferred time" group with three radio buttons (Morning, Afternoon, Evening). Make the date required, show an error if it is empty, include it in the error summary, and add the date and time to the dialog's confirmation text. Then repeat Step 7 on your version.

Show solution
HTML
<label for="date">Preferred date</label>
<span class="error" id="date-error"></span>
<input id="date" name="date" type="date" aria-describedby="date-error">

<fieldset>
  <legend>Preferred time</legend>
  <input type="radio" id="t-am" name="time" value="Morning" checked> <label for="t-am">Morning</label>
  <input type="radio" id="t-pm" name="time" value="Afternoon"> <label for="t-pm">Afternoon</label>
  <input type="radio" id="t-eve" name="time" value="Evening"> <label for="t-eve">Evening</label>
</fieldset>
JavaScript
// add to the rules array
rules.push({ id: "date", ok: (v) => v !== "", text: "Choose a preferred date" });

// in the success branch, before showModal()
const time = document.querySelector("input[name=time]:checked").value;
const date = document.querySelector("#date").value;
document.querySelector("#done-text").textContent += ` Requested slot: ${date}, ${time}.`;

The date field follows the same label, error and aria-describedby pattern as the others, so the existing loop adds it to the summary. The time choices are a fieldset with a legend, and the confirmation reads both values in the dialog.

Quick Quiz

1. Which problem in the original page blocked keyboard users from booking at all?

2. Why is an automated scan not enough for this audit?

3. Which WCAG criterion is failed by *:focus { outline: none; } with no replacement?

4. In what order should you usually fix the issues in an audit report?

Learn Web Accessibility with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call