Lab — Accessibility Audit
Run an automated audit on a provided broken page, then complete a keyboard and screen-reader test and fix the identified issues.
- Run axe DevTools (or Lighthouse) and document every WCAG violation with its criterion
- Identify keyboard navigation failures through a manual tab-through test
- Fix each violation in HTML and CSS
- Verify a clean automated audit after fixes are applied
This lab gives you a deliberately broken page and asks you to find every accessibility violation, fix it, and verify the fix. The goal is to build the muscle memory for the three-layer testing workflow: automated scan, keyboard test, then fix.
Work through each part in order. The fixes in Part 3 are most effective when they follow actual evidence from the tests rather than guesswork.
The broken page
Copy the HTML below into a new file called accessible-form.html and open it
in your browser. It has seven deliberate accessibility violations — your job is
to find and fix all of them.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Contact Us</title>
<style>
body { font-family: sans-serif; max-width: 600px; margin: 40px auto; padding: 0 16px; }
.error { color: red; }
button { padding: 8px 16px; }
* { outline: none; }
h3 { margin-top: 32px; }
label { display: block; margin-top: 16px; }
input, textarea { display: block; width: 100%; box-sizing: border-box; }
.required-note { color: #bbb; font-size: 0.85rem; }
</style>
</head>
<body>
<img src="logo.png">
<h1>Contact Us</h1>
<p class="required-note">Fields in red are required.</p>
<form>
<label style="color: red;">Full name</label>
<input type="text" id="name" />
<label for="email">Email address</label>
<input type="email" id="email" />
<label for="message">Message</label>
<textarea id="message" rows="5"></textarea>
<div style="margin-top: 24px; background: #e00; color: #f88; padding: 12px; display: none;" id="error-msg">
Please fill in all required fields.
</div>
<div onclick="submitForm()" style="margin-top: 16px; background: #0057b8; color: white; padding: 10px 20px; cursor: pointer; display: inline-block;">
Send message
</div>
</form>
<h3>Our office</h3>
<p>123 Main Street, Springfield</p>
<h5>Opening hours</h5>
<p>Monday – Friday, 9am – 5pm</p>
<script>
function submitForm() {
const name = document.getElementById('name').value;
if (!name) {
const msg = document.getElementById('error-msg');
msg.style.display = 'block';
}
}
</script>
</body>
</html>Part 1: automated audit
Open accessible-form.html in Chrome. Open DevTools and run either axe DevTools
(browser extension) or the Lighthouse Accessibility audit.
For each violation reported, record:
- The element or selector affected
- The WCAG success criterion violated (axe reports this as a rule ID; Lighthouse links to the criterion)
- The impact level (Critical / Serious / Moderate / Minor)
You should find at least five violations from the automated scan. If you find
fewer than four, check that you are scanning the full page and that the <html>
element does not have a lang attribute (it does not — that is one violation).
Automated tools will not flag every violation in this page. The heading
hierarchy problem (<h1> → <h3> → <h5>, skipping <h2> and <h4>) and
the <div> button issue may or may not be caught depending on which tool you
use. The keyboard test in Part 2 will surface those.
Part 2: keyboard test
Put the mouse away. Navigate the page using only Tab, Shift+Tab, Enter, and Space.
Work through these checks and note any failures:
- Skip link — press Tab on page load. Is there a skip link? (There is not — note this as a missing feature.)
- Focus visibility — as you tab through the page, can you see where focus
is at all times? (Look at the
outline: nonerule in the stylesheet.) - The button — can you reach the "Send message" element by pressing Tab? Can you activate it with Enter or Space?
- Form labels — tab to the "Full name" input. Read the label. Is the input
programmatically associated with its label? (It is not — the
<label>has noforattribute matching the input'sid.) - Error announcement — activate the button with Enter or Space (if you can reach it). Does the error message appear? If it does appear, does keyboard focus move to it or near it?
Part 3: fix each violation
Apply fixes to accessible-form.html. Here is the complete list of violations;
fix all of them:
Violation 1: Missing lang attribute
<!-- Before -->
<html>
<!-- After -->
<html lang="en">Violation 2: Image missing alt text
<!-- Before -->
<img src="logo.png">
<!-- After: meaningful description if the logo conveys the brand name -->
<img src="logo.png" alt="Acme Corp">
<!-- Or if purely decorative -->
<img src="logo.png" alt="">Violation 3: Outline removed without replacement
/* Before — removes all focus indicators */
* { outline: none; }
/* After — remove the wildcard rule entirely, or replace with a designed indicator */
:focus-visible {
outline: 3px solid #005fcc;
outline-offset: 2px;
}Violation 4: Form label not associated with its input
<!-- Before -->
<label style="color: red;">Full name</label>
<input type="text" id="name" />
<!-- After: add for/id linkage -->
<label for="name">Full name <span aria-hidden="true">*</span><span class="sr-only">(required)</span></label>
<input type="text" id="name" required />Violation 5: Color as the only indicator for required fields
<!-- Before -->
<p class="required-note">Fields in red are required.</p>
<!-- After: use text, not only color -->
<p class="required-note">Fields marked * are required.</p>Violation 6: Non-semantic <div> used as a button
<!-- Before -->
<div onclick="submitForm()" style="…">Send message</div>
<!-- After -->
<button type="button" onclick="submitForm()">Send message</button>Violation 7: Skipped heading levels
<!-- Before: h1 → h3 → h5 -->
<h1>Contact Us</h1>
…
<h3>Our office</h3>
…
<h5>Opening hours</h5>
<!-- After: h1 → h2 → h3 -->
<h1>Contact Us</h1>
…
<h2>Our office</h2>
…
<h3>Opening hours</h3>Bonus: error message not announced to screen readers
The error <div> becomes visible on failure but receives no screen reader
announcement. Add role="alert" so it is announced when content is injected:
<div role="alert" id="error-msg" style="display: none; …">
Please fill in all required fields.
</div>Self-review
After applying all fixes, run the automated audit again. The scan should return zero Critical and Serious violations. If any remain, read the violation detail and apply the same diagnostic process: identify the element, identify which WCAG criterion is violated, and fix the root cause in the HTML or CSS.
The test for a correct fix is not "the tool stopped complaining." It is "a keyboard user can complete this task" and "a screen reader user hears something meaningful at every step." Re-run the keyboard test from Part 2 after your fixes and confirm each check passes.
Going further
If you have VoiceOver (macOS) or NVDA (Windows) available, enable it and navigate the fixed page. Listen to:
- What is announced when focus enters each form input
- What is announced when the error message appears after submitting with an empty name field
- Whether the heading structure (now
<h1>→<h2>→<h3>) is navigable via the rotor in VoiceOver (Control + Option + U, then arrow to Headings)
This turns the fix exercise into a full three-layer audit and builds the sensory reference point for what "accessible" actually sounds like.
Testing Accessibility
Automated tools catch roughly half of accessibility issues — the rest require keyboard testing and a screen reader, because tools can't judge intent.
Parsing and the Render Tree
Before a pixel is painted, the browser parses HTML into the DOM and CSS into the CSSOM — then merges them into a render tree that contains only what will actually be painted.