POUR in Practice: Reading a WCAG Success Criterion Like a Test Case
qa-accessibility-wcag-fundamentals-pour -->
A stakeholder asks whether the app is "WCAG compliant," and the honest answer is only meaningful once you can name which criteria, at which level, on which pages. WCAG is not a vibe — it is a numbered checklist, and each item is written in a form you can turn directly into a test case.
WCAG organizes its 80-odd success criteria under four principles, remembered as POUR:
| Principle | Meaning | Fails when |
|---|---|---|
| Perceivable | Users can sense the content | Images with no alt text; text at 2.1:1 contrast; a video with no captions |
| Operable | Users can operate the interface | A date picker only reachable by mouse; a modal with no escape key |
| Understandable | Behavior is predictable | An error message that says "invalid input" without saying which field |
| Robust | Any user agent can parse it | A button announced as "text" because it is a <div onclick> |
Each criterion has a stable identifier you will see in bug reports and audit tooling: <principle>.<number>.<number>. Read one out loud — criterion 1.4.3, Contrast (Minimum), Level AA:
"The visual presentation of text and images of text has a contrast ratio of at least 4.5:1, except for … large-scale text … which has a ratio of at least 3:1."
Decompose it into the parts a test needs: 1.4.3 is the address, the ratio thresholds (4.5:1 for normal text, 3:1 for large text — 24 px, or 18.66 px bold) are the assertion, and the exceptions (logotypes, incidental text) are the escape hatches you must justify, not assume. That decomposition is the whole reading skill; every criterion decomposes the same way.
Conformance is defined at three levels, and the levels stack: A is the floor, AA adds criteria and is the level referenced by Section 508, the EU's EN 301 549, and most enterprise procurement — so AA is the default target for any serious product. AAA is a stretch goal; the spec itself says AAA is not intended for all sites. Two scoping rules trip up first-time auditors: conformance requires every page (not a sample) to satisfy the level, and a complete process must be accessible — you cannot claim AA while the checkout payment step is keyboard-trappable, even if every other page passes.
Version discipline matters in interviews and in contracts. WCAG 2.1 AA is the most widely cited target; WCAG 2.2 (published October 2023) added nine criteria — including 2.4.11 Focus Not Obscured, 2.5.7 Dragging Movements, and 2.5.8 Target Size (Minimum), which demands a 24×24 CSS px hit area — and dropped 4.1.1 Parsing as obsolete. A contract saying "WCAG 2.2 AA" therefore legally includes the drag-free and target-size demands that 2.1 never had. When a ticket says "a11y bug," your first move is to cite the criterion: "fails 2.1.1 Keyboard (Level A)" is actionable and verifiable; "not accessible" is neither.
That citation habit also settles arguments about scope. Product pushes back that the custom dropdown "works fine with a mouse," and the criterion answer is clean: 2.1.1 Keyboard (A) has no mouse exception. Analyzing Results and Bottlenecks measured systems against numbers; accessibility measures interfaces against criteria, and the next chapter opens with the cheapest, highest-yield test you can run — the ten-minute keyboard-only pass that needs no tooling.
The Keyboard-Only Pass: Finding the Focus Trap Before a User Does
qa-accessibility-wcag-fundamentals-keyboard -->
Screen-reader users, people with motor injuries, and power users all drive the interface from the keyboard — and so do your users on the day a trackpad driver breaks. A keyboard-only pass costs ten minutes, needs no tooling, and finds a class of bugs no automated audit will ever report. Put the mouse in a drawer and work through the page with Tab, Shift+Tab, Enter, Space, and Esc.
What you are watching for, mapped to criteria:
| Observation | Criterion | What it looks like |
|---|---|---|
| Every control reachable, in a sensible order | 2.1.1 (A), 2.4.3 (A) | Tab jumps from header straight to the footer, skipping the nav; focus order runs visually backwards |
| Nothing strands you | 2.1.2 No Keyboard Trap (A) | Inside a modal, Tab cycles the backdrop forever; Esc must close it |
| Focus is always visible | 2.4.7 Focus Visible (AA) | The browser outline is stripped (outline: none) and you are tabbing blind |
| Focus isn't hidden under sticky UI | 2.4.11 Focus Not Obscured (AA, WCAG 2.2) | The focused field slides under a sticky header or a cookie banner |
| Targets big enough to hit | 2.5.8 Target Size (AA, WCAG 2.2) | Two icon buttons 12 px apart; hit area under 24×24 CSS px |
Two DOM details cause most order-and-reachability failures. tabindex="0" inserts an element into the natural tab order — legitimate for a custom widget, but every positive value (tabindex="3") overrides the document order and reshuffles focus across the whole page as the browser sorts every positive value before everything else. The rule: never use positive tabindex; reorder the DOM instead. tabindex="-1" removes an element from tabbing while keeping it programmatically focusable — exactly right for the <h1> you jump to from a skip link:
<body>
<a class="skip-link" href="#main">Skip to main content</a>
<header>…</header>
<main id="main" tabindex="-1">
<h1 tabindex="-1">Orders</h1>
The skip link is criterion 2.4.1 (Bypass Blocks, Level A): it lets keyboard users hop the repeated navigation on every page. Make sure it is visible when focused — a skip link hidden with display: none or opacity: 0 is announced by no one and helps no one; the standard pattern is to position it off-screen and slide it in on :focus.
Modals are the richest source of traps. A correct dialog does four keyboard things: moves focus into it on open, cycles Tab within it (a focus trap — here trapping is the requirement, opposite of 2.1.2), restores focus to the trigger on close, and closes on Esc. Test all four. Then test the page behind it: with aria-hidden="true" missing on the background content, a screen reader happily reads the page under the open dialog, which is criterion 4.1.2 Name, Role, Value territory — the subject of the next chapter, where you hear what assistive technology announces rather than inferring it from the DOM.
The keyboard pass is manual by nature — the tab order is the DOM's order plus CSS order plus script, and no static scanner can execute it. What scanners can do is the complement of this chapter, and that division of labor is exactly what Automated Accessibility Audits quantifies.
What NVDA Actually Announces: Alt Text, Labels, and Contrast Numbers
qa-accessibility-wcag-fundamentals-screenreaders -->
A DOM that "looks right" can announce as garbage. The only way to know what a screen-reader user hears is to run one — and the two you should learn are NVDA on Windows with Firefox (free, from nvaccess.org) and VoiceOver on macOS with Safari (built in; toggle with Cmd+F5). On mobile, VoiceOver (iOS, triple-click the side button) and TalkBack (Android, hold both volume keys) round out the set.
Learn ten NVDA commands and you can audit anything: Insert+Down reads from the cursor; h jumps between headings, k links, g graphics, f form fields, t tables; Tab moves between focusable controls; Insert+F7 opens the Elements List for a structured view of links and headings. That single-letter vocabulary is why semantic HTML is an accessibility feature: NVDA's h/k/g shortcuts traverse roles, so a <div class="heading"> simply does not exist to the user, and criterion 1.3.1 Info and Relationships (A) is exactly the demand that relationships be expressed in markup.
Three announcement defects account for most screen-reader bugs:
- Images with no usable alt. Criterion 1.1.1 (A) requires alt text for informative images and empty
alt=""for decorative ones. Missingaltmakes NVDA announce the file path (img_2049_final dot png);alt="icon"announces "icon," which is noise. Write alt text that carries the function or information: a product photo in a catalog getsalt="Nikon Z6 II, black, with 24–70mm lens", a decorative divider getsalt="". - Inputs with no label. Criterion 3.3.2 Labels or Instructions (A). A placeholder is not a label: it vanishes on input, fails contrast in most browsers, and is often skipped by older assistive tech. The association is what matters —
<label for="email">pointing at the input, oraria-label/aria-labelledbywhen a visible label is genuinely impossible. - Custom controls with no state. Criterion 4.1.2 Name, Role, Value (A). A
<div class="menu">announces as nothing; a real disclosure button announces "Collapse navigation, button, expanded." The minimum viable ARIA set for interactive widgets is a role, a name, and a state:
<button aria-expanded="false" aria-controls="filters">Filters</button>
<div id="filters" hidden>…</div>
<!-- announcements that follow a click: "Filters, button, collapsed" → "…, expanded" -->
Dynamic updates need live regions (1.3.1/4.1.3): aria-live="polite" for a result count ("23 items found"), assertive reserved for errors. A page that re-renders results silently is, to a screen reader, a page that did nothing.
Two sensory checks complete the manual sweep. Contrast: open DevTools, pick the text color, and the contrast panel prints the ratio against the background — you are asserting 4.5:1 for normal text and 3:1 for large text (1.4.3, AA), and 3:1 for UI component boundaries like input borders (1.4.11, AA). Watch the non-obvious cases: placeholder text, disabled-looking elements that are still interactive, and text over images, where the measured pixel pair is the only one that counts. Zoom and reflow (1.4.10, AA): press Ctrl/Cmd and + to 200 % and confirm nothing is lost — no horizontal scrolling for content, no clipped controls. Everything you just verified by ear and eye is only the half of the job a machine can do on every commit; that half is the next topic, Automated Accessibility Audits.
Quiz
1. A modal opens on click. Which keyboard behavior does the dialog pattern and criterion 2.1.2 require?
2. Which text/background pair passes WCAG 2.1 AA (criterion 1.4.3) for normal-size body text?
3. A developer adds tabindex="3" to the primary CTA so it is focused first. What is wrong?
4. An axe-core scan is fully green. Which defect could still be present?
Flashcards
Question
The POUR principles, with one failure mode each.
Click to reveal answer
Answer
Perceivable: image without alt, text at 2.1:1 contrast. Operable: mouse-only date picker, modal with no Esc. Understandable: "invalid input" without naming the field. Robust: a <div onclick> announced as text because it carries no role or name.
Question
Conformance levels A / AA / AAA, and the AA default.
Click to reveal answer
Answer
Levels stack: A is the floor, AA adds criteria and is referenced by Section 508, EN 301 549, and most procurement, so AA is the default target; AAA is not meant for whole sites. Conformance requires every page and every complete process (e.g., all of checkout) to pass — sampling does not count.
Question
The keyboard-only pass checklist.
Click to reveal answer
Answer
Tab/Shift+Tab reach every control in sensible order (2.1.1, 2.4.3); Esc always exits dialogs with no keyboard trap (2.1.2); focus outline visible (2.4.7) and not hidden under sticky UI (2.4.11); targets at least 24x24 CSS px (2.5.8); skip link visible on focus (2.4.1). Never use positive tabindex.
Question
Contrast thresholds by content type.
Click to reveal answer
Answer
Normal text 4.5:1 and large text (24 px, or 18.66 px bold) 3:1 under 1.4.3 (AA); UI component boundaries and meaningful graphics 3:1 under 1.4.11. DevTools prints the computed ratio; #767676 on white is the canonical minimum-passing gray.
Question
What a screen-reader session verifies that the DOM cannot.
Click to reveal answer
Answer
Whether announcements carry meaning: alt text that informs (not "icon"), labels that are associated and persistent (placeholders are not labels), role + name + state on custom widgets (aria-expanded), live regions for dynamic updates, and real semantics so single-letter navigation (h/k/g) works.
Revision Notes
Key Takeaways
- 1. WCAG is a numbered checklist: cite criterion id, level, threshold, and exception — "not accessible" is not a bug report
- 2. AA is the practical target everywhere; conformance requires every page and every complete process, so partial checkout failures sink the claim
- 3. The keyboard-only pass finds reachability, order, trap, and visibility bugs that no scanner can execute or detect
- 4. Semantics are functionality: roles power screen-reader navigation, so a div-based widget is invisible to the users who depend on it
- 5. Verify announcements and contrast with real tools (NVDA/VoiceOver, DevTools panel) — presence in the DOM is not the same as meaning
Interview Tips
- • "What is WCAG?" — four principles, numbered criteria, levels A/AA/AAA; say AA is the default target and cite how a criterion decomposes into a testable assertion
- • "How do you test accessibility?" — keyboard pass, screen-reader session on changed flows, contrast and zoom checks, then automated scans on every PR; lead with the manual half
- • "Is a green axe run accessible?" — no; tooling covers roughly a third — structure, names, contrast — while focus order, operability, and meaning are human checks
- • "A modal opens and my keyboard is stuck — which criterion?" — 2.1.2 No Keyboard Trap (A), plus the dialog pattern: focus in, Tab cycles, Esc closes, focus restored
Cheat Sheet
WCAG Accessibility Fundamentals
POUR: Perceivable · Operable · Understandable · Robust. Criteria are addressed like 1.4.3 (principle.number.sub): quote the id, the threshold, and the exceptions in every a11y bug.
Levels: A (floor) → AA (default target: Section 508 / EN 301 549 / procurement) → AAA (stretch, not for whole sites). WCAG 2.2 (Oct 2023) adds 2.4.11 Focus Not Obscured, 2.5.7 Dragging Movements, 2.5.8 Target Size 24×24 px; drops 4.1.1. Conformance covers every page and every complete process.
Keyboard pass (10 min, no tools):
| Check | Criterion |
|---|---|
| All controls reachable, sensible order | 2.1.1 / 2.4.3 (A) |
| Esc closes dialogs, no trap | 2.1.2 (A) |
| Focus visible, not under sticky UI | 2.4.7 / 2.4.11 (AA) |
| Skip link visible on focus | 2.4.1 (A) |
| Hit areas ≥ 24×24 CSS px | 2.5.8 (AA, 2.2) |
tabindex: never positive (reorders the page), 0 = join natural order, -1 = programmatically focusable only.
Screen reader (NVDA/Firefox, VoiceOver/Safari): Insert+Down read · h/k/g/f/t jump · Insert+F7 elements list. Verify alt (1.1.1 — decorative gets alt=""), associated labels (3.3.2), role/name/state + aria-expanded (4.1.2), live regions for async updates. Placeholder ≠ label.
Sensory: contrast 4.5:1 text / 3:1 large (1.4.3), components 3:1 (1.4.11); 200 % zoom with no loss (1.4.10 reflow).