BXUP Track progress & test yourself — free
Frontend & Mobile · Hands-on lab · ~4 min read
FRONTEND & MOBILE · HANDS-ON LAB

Lab 07 · Three-Layer Accessibility Audit

Audit a seeded page carrying ten accessibility defects — four catchable by automated scan, four needing AI-guided checks, two visible only to a human with a keyboard and a screen reader. Pass requires finding all ten, mapping each to WCAG, and correctly flagging the two machine-invisible ones.

Marcus runs frontend for a public-sector portal, where accessibility isn't a nice-to-have — it's a legal exposure. His team's CI runs axe and reports "0 violations," and his product manager reads that as "accessible." It isn't: automated scanners detect roughly a third to half of real WCAG failures; the rest need guided judgment or a human at the controls. This lab makes that boundary tangible. You get one page with ten planted defects distributed across the three detection layers, and your audit is graded not just on finding them, but on knowing which layer each one needed.

Time & tier

~90 minutes hands-on · Verify stage (maps to module M5, playbook PB-29) · $0 — axe DevTools free tier or @axe-core/cli, any AI assistant, and the screen reader already on your machine (VoiceOver on macOS, NVDA free on Windows).

Setup

mkdir a11y-lab && cd a11y-lab
npm init -y && npm install -D @axe-core/cli serve

The page under audit, index.html. Type or paste it exactly — every character is load-bearing:

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Grantly — Apply for a small-business grant</title>
  <style>
    body { font-family: system-ui; margin: 0; padding: 2rem; max-width: 720px; }
    .hint { color: #999999; background: #ffffff; font-size: 0.875rem; }
    .icon-btn { border: none; background: none; font-size: 1.25rem; cursor: pointer; }
    .steps { display: flex; flex-direction: row-reverse; gap: 1rem; }
    .toast { min-height: 1.5rem; color: #b91c1c; }
    input, select { display: block; margin: 0.25rem 0 1rem; padding: 0.5rem; }
    a { color: #4f46e5; }
  </style>
</head>
<body>
  <h1>Apply for a small-business grant</h1>
  <h4>Before you start</h4>
  <p class="hint">Applications close on 30 September. Processing takes 6 weeks.</p>
  <div class="steps">
    <div><b>Step 3</b><br>Review and submit</div>
    <div><b>Step 2</b><br>Financial details</div>
    <div><b>Step 1</b><br>Business profile</div>
  </div>
  <img src="grant-flow.png" width="680">
  <p>Need the rules? <a href="/rules.pdf">Click here</a>.</p>
  <form>
    <label for="biz">Business name</label>
    <input id="biz" type="text">
    <input type="text" placeholder="ABN / registration number">
    <label for="state">State</label>
    <select id="state"><option>NSW</option><option>VIC</option><option>QLD</option></select>
    <button class="icon-btn" type="button" onclick="save()">💾</button>
    <button type="submit" tabindex="2">Continue</button>
    <button type="button" tabindex="1" onclick="history.back()">Back</button>
  </form>
  <p class="toast" id="toast"></p>
  <script>
    function save() {
      document.getElementById("toast").textContent =
        "Draft could not be saved. Fix the errors above.";
    }
  </script>
</body>
</html>

Serve it with npx serve . (note the port it prints). The findings instrument, audit.md:

| # | Defect | WCAG SC | Layer that caught it (auto / guided / manual) |
|   |        |         | Severity | Fix |
Layers: AUTO = scanner reported it. GUIDED = AI-assisted structured check
        surfaced it. MANUAL = only keyboard/screen-reader use revealed it.
Rule: record each defect under the FIRST layer that caught it.

Guided steps

  1. Layer 1 — automated scan.npx axe http://localhost:3000 --exit (match your serve port). Expected: a violations list with rule IDs and nodes. Transfer each distinct defect into audit.md as an AUTO row with its WCAG success criterion (axe gives you the mapping). Expected count: about four distinct real defects. Note the number — you will need the "axe found N of 10" figure later.
  2. Layer 2 — AI-guided structured checks. Paste the full HTML into your assistant with: "Act as a WCAG 2.2 AA auditor. The automated scan already reported [paste axe's rule IDs]. Now check what scanners typically miss: heading structure and hierarchy, link purpose, focus and tab order, status messages and how updates are announced, and reading order versus visual order. For each issue: the element, the WCAG SC, why a scanner missed it, and the fix." Interrogate anything vague — "which SC exactly?", "show me the failing element." Add each verified finding as a GUIDED row. Verify means you confirmed it in the page yourself, not that the model asserted it; log any hallucinated finding the model produced too.
  3. Layer 3a — keyboard pass. Hands off the mouse. Tab from the address bar through the whole page. Narrate into audit.md: what order does focus move in? Does it match the visual and logical order? Can you reach and operate every control? Press the save button via keyboard and watch what happens — did anything tell you what changed?
  4. Layer 3b — screen-reader pass. VoiceOver (Cmd-F5) or NVDA. Traverse the page top to bottom by element, then by heading. Listen for: how the steps list reads, what the image announces, what the icon button announces, what happens when the save action updates the toast. Every mismatch between what a sighted user sees and what you hear is a candidate MANUAL row.
  5. Reconcile to ten. You should now have ten distinct defects. If short, the gaps are almost always in: reading order, announced-vs-visible state changes, or focus order. Re-run steps 3–4 on those themes specifically. Do not open the answer key to complete the table.
  6. Flag the two machine-invisible defects. Mark exactly two rows as "no tool could catch this" and write one sentence each on why — what information the failure depends on that neither a DOM scanner nor a code-reading LLM possesses. Getting these two right is a pass condition.
  7. Remediate. Create index.fixed.html fixing all ten. Re-run axe (expect 0 violations), redo the keyboard tab-through and a 2-minute screen-reader spot check on the steps, the save button, and the toast. Automated-clean plus manual-verified is the definition of done here.
  8. Write the coverage summary. Three sentences at the bottom of audit.md: N of 10 caught by axe; M more by guided checks; the remainder by hand — and what this ratio means for a team whose CI gate is "axe passed."
Warning

"AI made it" is not an accessibility defense, and neither is "the scanner passed." If this page shipped, the legal exposure attaches to the org regardless of which tool — or model — wrote the markup.

Verify

  • audit.md has exactly ten defect rows, each with element, WCAG SC, severity, first-catching layer, and fix.
  • The layer distribution is approximately 4 AUTO / 4 GUIDED / 2 MANUAL (record your actual split; axe versions shift the boundary by ±1 and that's fine if you justify it).
  • The two MANUAL rows each have a written why-machines-miss-this sentence.
  • Any hallucinated findings from the step-2 model session are logged and marked rejected.
  • index.fixed.html: axe reports zero violations and your keyboard/screen-reader spot check passes.

Self-grading rubric (pass = 8/12)

  • Detection (0–5): all ten found without the key = 5; nine = 4; eight = 3.
  • Layer attribution (0–3): every row recorded under the first layer that genuinely caught it, machine-invisible pair correctly identified = 3; pair wrong = max 1.
  • WCAG mapping (0–2): SC numbers correct on at least eight rows = 2.
  • Remediation (0–2): fixed page passes all three layers = 2.
▸Answer key — all ten seeded defects by layeroptional deep dive — click to expand

AUTO — the scanner catches these four:

  • 1. Insufficient contrast..hint is #999999 on white — ratio ≈ 2.8:1, needs 4.5:1. WCAG 1.4.3. Fix: darken to ≥ #767676 (4.54:1) or the design system's muted token.
  • 2. Image without alt.grant-flow.png has no alt. WCAG 1.1.1. Fix: meaningful alt (it's informative — it depicts the flow) or alt="" only if truly redundant with the steps text.
  • 3. Form field without a label. The ABN input has only a placeholder. WCAG 3.3.2 / 4.1.2 (axe: label). Placeholder disappears on input and isn't reliably announced. Fix: a real <label for>.
  • 4. Button with no accessible name. The 💾 icon button contains only an emoji glyph; axe flags button-name (and some versions also flag the emoji announcement). WCAG 4.1.2. Fix: aria-label="Save draft" (and aria-hidden="true" on the glyph if kept).

GUIDED — a structured AI-assisted pass catches these four; scanners don't or only warn:

  • 5. Heading level skip.h1 → h4 with no h2/h3. WCAG 1.3.1 (axe treats heading-order as a best-practice warning, not a violation — which is why it lands in this layer). Fix: make it h2.
  • 6. Ambiguous link text. "Click here" fails link-purpose-in-context. WCAG 2.4.4. Scanners can't judge semantics; a guided check catches it instantly. Fix: "Read the grant rules (PDF)".
  • 7. Positive tabindex focus-order sabotage.tabindex="2" on Continue and tabindex="1" on Back yank focus to Back before Continue and before the form fields' natural order. WCAG 2.4.3. A code-reading model flags positive tabindex reliably; axe's tabindex rule is best-practice-level. Fix: remove both, order the DOM correctly.
  • 8. Status message not announced. The toast is updated via textContent with no role="status"/aria-live, so the save failure is silent in a screen reader. WCAG 4.1.3. A guided check on "how are updates announced?" surfaces it from the code; no scanner executes the interaction. Fix: role="status" (or role="alert" given it's an error) on the toast element.

MANUAL — only a human at the controls catches these two (the machine-invisible pair):

  • 9. Visual order contradicts DOM/reading order..steps uses flex-direction: row-reverse: sighted users see Step 1 → 2 → 3, but the DOM — hence tab order, reading order, and screen-reader order — is 3 → 2 → 1. Nothing is invalid in the markup; the failure exists only in the relationship between rendered geometry and source order, which a DOM scanner doesn't model and a code-reading LLM can only guess at without rendering. WCAG 1.3.2 / 2.4.3. Caught by the keyboard/SR traversal in steps 3–4. Fix: reverse the DOM, drop row-reverse.
  • 10. Silent state change at the point of interaction. Even after you know about defect 8, the experience failure — press save, hear nothing, no focus move, no announced feedback, error referenced "above" that doesn't exist visually near focus — is verifiable only by operating the page with the screen reader running. It's the difference between "an aria-live attribute is missing" (code fact) and "a blind user cannot know their draft failed to save" (experience fact). WCAG 4.1.3 / 3.3.1. Full credit for the machine-invisible flag requires articulating that distinction (or an equivalent framing).

Scoring: Detection is graded against the ten above; defects 9 and 10 must be your two MANUAL flags for full attribution credit. Common miscounts: splitting defect 7 into two rows (it's one failure), or claiming the h4 skip as AUTO — check whether your axe run actually failed on it or merely warned.

Key Takeaway:

Automated scans catch the markup facts, guided AI checks catch the semantic judgments, and only a human with a keyboard and a screen reader catches the experience failures — an accessibility claim is only as strong as the deepest layer you actually ran.

This is the free reading view. Open BXUP to track your progress, run the hands-on labs, and prove it with original, exam-pattern assessments.
© Anvi Innovate · BXUP — Be Exponentially Upskilled · BXUP is an independent learning platform, not affiliated with or endorsed by AWS, Microsoft, Google, NVIDIA, IBM, Salesforce, Databricks, Oracle or Anthropic. All trademarks belong to their respective owners. · bxup.in