Accessibility Testing
Many people use software differently from the way its developers do: with a screen reader that reads the page aloud, with only a keyboard, with zoomed text, with colour blindness, or with a shaky hand on a small touch screen. Accessibility testing checks that these users can perceive, understand and operate your product. It also tends to improve the product for everyone: clear labels, good contrast and big tap targets help a shopkeeper in bright sunlight as much as a user with low vision.
In this lesson you will learn the WCAG standard in brief, a manual testing checklist, and simple automated checks you can run, including a contrast calculator.
WCAG in brief
The Web Content Accessibility Guidelines (WCAG) from the W3C are the international reference. The current version is WCAG 2.2, a W3C Recommendation first published in October 2023 (an updated edition is dated December 2024). WCAG groups its guidance under four principles, often remembered as POUR:
| Principle | Meaning | Examples of checks |
|---|---|---|
| Perceivable | Users can see or hear the content in some way | Text alternatives for images, captions, enough colour contrast |
| Operable | Users can work every control | Full keyboard access, no keyboard traps, enough time, large enough targets |
| Understandable | Content and behaviour are clear | Labels and instructions, clear error messages, predictable navigation |
| Robust | Assistive technologies can interpret it | Correct roles, names and states for controls |
Each guideline has testable success criteria at three levels: A (minimum), AA (the usual target for websites and in many laws and policies) and AAA (highest). For example, criterion 1.4.3 requires a contrast ratio of at least 4.5:1 for normal text and 3:1 for large text at level AA. WCAG 2.2 added criteria such as Focus Not Obscured, Dragging Movements, Target Size (Minimum), Redundant Entry and Accessible Authentication, and removed the old 4.1.1 Parsing criterion.
In India, the Rights of Persons with Disabilities Act, 2016 makes accessibility a legal topic as well as a quality one; check your organisation's policy for the exact standard it must meet.
Manual checks every tester can do
Automated tools find only part of the problems. These manual checks need no special skills:
- Keyboard only. Put the mouse away. Can you reach every link, button and field with Tab, operate them with Enter or Space, and always see where the focus is? Can you get out of every menu and dialog?
- Zoom to 200% and enlarge the phone's font size. Does text overlap or disappear?
- Screen reader. Try NVDA (free, Windows), Narrator (Windows), VoiceOver (macOS and iOS) or TalkBack (Android). Are images, buttons and form fields announced with sensible names?
- Colour alone. Is any information shown only by colour, such as "fields in red are wrong"? Add text or icons.
- Forms and errors. Does every field have a visible label? Are error messages specific and announced?
- Time limits and motion. Can users get more time for an OTP? Can moving content be paused?
Automated checks you can run
Many accessibility problems are visible in the HTML itself: images without alt text, inputs without labels, buttons without a name, and headings that skip levels. This small checker uses Python's built-in HTML parser to find them:
from html.parser import HTMLParser
page = """
<html lang="en">
<body>
<h1>Kirana Kart</h1>
<img src="logo.png" alt="Kirana Kart logo">
<img src="offer-banner.png">
<h3>Today's offers</h3>
<label for="email">Email</label>
<input id="email" type="email">
<input id="phone" type="tel" placeholder="Phone number">
<button><img src="cart.svg"></button>
<button>Pay ₹1,299</button>
</body>
</html>
"""
class A11yChecker(HTMLParser):
def __init__(self):
super().__init__()
self.problems = []
self.labels = set()
self.inputs = []
self.last_heading = 0
self.in_button = False
self.button_has_name = False
def handle_starttag(self, tag, attrs):
a = dict(attrs)
line = self.getpos()[0]
if tag == "img" and "alt" not in a:
if self.in_button:
return
self.problems.append(f"line {line}: <img> has no alt text")
if tag == "label" and "for" in a:
self.labels.add(a["for"])
if tag == "input":
self.inputs.append((line, a.get("id")))
if tag in ("h1", "h2", "h3", "h4", "h5", "h6"):
level = int(tag[1])
if self.last_heading and level > self.last_heading + 1:
self.problems.append(f"line {line}: heading jumps from h{self.last_heading} to h{level}")
self.last_heading = level
if tag == "button":
self.in_button, self.button_has_name = True, False
self.button_line = line
def handle_data(self, data):
if self.in_button and data.strip():
self.button_has_name = True
def handle_endtag(self, tag):
if tag == "button":
if not self.button_has_name:
self.problems.append(f"line {self.button_line}: <button> has no accessible name")
self.in_button = False
checker = A11yChecker()
checker.feed(page)
for line, input_id in checker.inputs:
if input_id not in checker.labels:
checker.problems.append(f"line {line}: <input id={input_id}> has no <label>")
for problem in sorted(checker.problems, key=lambda p: int(p.split()[1].rstrip(":"))):
print(problem)line 6: <img> has no alt text line 7: heading jumps from h1 to h3 line 10: <input id=phone> has no <label> line 11: <button> has no accessible name
Line numbers count from the start of the page text. The phone field has a placeholder, but a placeholder disappears when you type and is not a reliable label. The cart button contains only an icon, so a screen reader would announce just "button". Fixes: alt="Diwali offer: 20% off sweets", an <h2> instead of <h3>, a <label> for the phone field, and aria-label="Cart" or visible text on the icon button.
Checking colour contrast
Contrast is calculated from the relative luminance (perceived brightness) of the text and background colours: ratio = (lighter + 0.05) / (darker + 0.05). It ranges from 1:1 (same colour) to 21:1 (black on white).
function luminance(hex) {
const [r, g, b] = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255);
const lin = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
}
function contrast(fg, bg) {
const [hi, lo] = [luminance(fg), luminance(bg)].sort((a, b) => b - a);
return (hi + 0.05) / (lo + 0.05);
}
const pairs = [
["#000000", "#ffffff", "black on white"],
["#767676", "#ffffff", "mid grey on white"],
["#999999", "#ffffff", "light grey on white"],
["#ffffff", "#0f766e", "white on teal button"],
["#f4c430", "#ffffff", "saffron text on white"],
];
for (const [fg, bg, label] of pairs) {
const ratio = contrast(fg, bg);
const normal = ratio >= 4.5 ? "pass" : "FAIL";
const large = ratio >= 3 ? "pass" : "FAIL";
console.log(`${ratio.toFixed(2).padStart(5)}:1 normal text ${normal}, large text ${large} (${label})`);
}21.00:1 normal text pass, large text pass (black on white) 4.54:1 normal text pass, large text pass (mid grey on white) 2.85:1 normal text FAIL, large text FAIL (light grey on white) 5.47:1 normal text pass, large text pass (white on teal button) 1.64:1 normal text FAIL, large text FAIL (saffron text on white)
The light grey placeholder-style text that designers love fails, and so does bright saffron text on white, even though both look fine to many people on a good monitor.
Tools
- axe-core is a widely used open-source accessibility rules engine. It runs in browser extensions (axe DevTools) and inside automated tests.
- Lighthouse in Chrome DevTools includes an accessibility audit.
- WAVE and browser accessibility inspectors show the names and roles that assistive technology sees.
With Playwright Test and the @axe-core/playwright package, an automated scan becomes part of your suite:
import { test, expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";
test("login page has no detectable WCAG A/AA violations", async ({ page }) => {
await page.goto("http://localhost:8000/login.html");
const results = await new AxeBuilder({ page })
.withTags(["wcag2a", "wcag2aa"])
.analyze();
expect(results.violations).toEqual([]);
});Automated scans catch only a portion of WCAG problems. Whether alt text is meaningful, whether the focus order makes sense and whether an error message is understandable still needs a person.
Common mistakes
- Relying only on an automated score. A perfect score does not mean the page is usable with a keyboard or a screen reader.
- Placeholders instead of labels. They vanish on typing and are often low contrast.
- Removing the focus outline for looks, which leaves keyboard users lost.
- Testing accessibility only at the end. Fixing a design is far cheaper than rebuilding finished screens.
Key points
- Accessibility testing checks that people with disabilities can perceive, operate and understand the product.
- WCAG 2.2 is the current standard; level AA is the usual target, with POUR as its four principles.
- Keyboard-only, zoom, screen reader and colour checks are manual tests any tester can do.
- Contrast must be at least 4.5:1 for normal text and 3:1 for large text at level AA.
- axe-core, Lighthouse and WAVE automate part of the work, but human judgement is still needed.
Exercise
A teal brand colour #14b8a6 is proposed for white button text (#ffffff on #14b8a6). Use the contrast program to check it. If it fails, try darker teals (#0d9488, #0f766e) and report the lightest one that passes for normal text.
Show solution
for (const bg of ["#14b8a6", "#0d9488", "#0f766e"]) {
const ratio = contrast("#ffffff", bg);
console.log(bg, ratio.toFixed(2), ratio >= 4.5 ? "pass" : "FAIL");
}#14b8a6 2.49 FAIL #0d9488 3.74 FAIL #0f766e 5.47 pass
Add these lines after the two functions in the contrast program. The two lighter teals fail for normal text (#0d9488 would pass only for large text, at 3:1 or more), so #0f766e is the lightest of the three that can be used behind white button labels.
Quick Quiz
1. Which minimum contrast ratio does WCAG level AA require for normal-size text?
4.5:1 is the AA minimum for normal text; large text needs 3:1.
2. What does the P in POUR stand for?
The four principles are Perceivable, Operable, Understandable and Robust.
3. Which is the best way to label a phone number field?
Labels stay visible and are announced by screen readers; placeholders disappear.
4. An automated scan reports zero violations. What should you conclude?
Tools catch only part of the problems; keyboard and screen reader checks remain essential.
Learn Software Testing with a mentor
Free lessons get you started. Our Software Testing training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
