Chapter 14
Web Accessibility
WCAG, keyboard support, ARIA patterns, colour contrast and testing.
# 14. Web Accessibility
Accessibility is not a specialized feature layered onto a finished interface. It is a design and engineering requirement that affects content structure, interaction patterns, styling, and testing. When a website is accessible, it is generally easier to use, easier to understand, and more resilient in the face of different devices, input methods, and user needs. In production work, accessibility is a quality bar for shipping user-facing software, not a last-minute checklist item.
This chapter introduces the Web Content Accessibility Guidelines (WCAG) 2.2, the POUR principles, keyboard interaction patterns, the first rule of ARIA, focus management, contrast, accessible dialogs, and practical screen-reader testing workflows.
Overview and Learning Objectives
By the end of this chapter, you will be able to:
- Explain the WCAG 2.2 principles and the three conformance levels.
- Design keyboard operable interfaces using focus, Tab order, and visible focus styles.
- Understand the first rule of ARIA and decide when ARIA is helpful versus unnecessary.
- Use semantic roles, states, and properties appropriately on interactive components.
- Create accessible modals with focus trapping and escape behavior.
- Evaluate color contrast and readability under real-world conditions.
- Test with NVDA and VoiceOver using a structured methodology.
WCAG 2.2 and the POUR Principles
WCAG 2.2 is the current accessibility standard widely used around the world. It defines requirements that websites and applications should meet to be usable by people with diverse abilities. The guidelines are organized around four core principles: perceivable, operable, understandable, and robust. In shorthand, this is often called POUR.
Perceivable
Information and user interface components must be presented in ways users can perceive.
This includes:
- text alternatives for non-text content
- captions and other alternatives for media
- structure and semantics that support screen readers
- sufficient color contrast and adaptable layouts
- ability to present content in different ways without losing information
For example, if a page has an icon-only button that reveals a menu, the button needs an accessible name. Otherwise, assistive technology cannot announce what the control does. A red alert indicator alone is not enough; there should also be text or a visible label that users can perceive without relying on color alone.
Operable
User interface components and navigation must be operable.
This means:
- all functionality must be available from a keyboard
- users are not trapped in one part of the interface
- users have enough time to read and use content
- content does not cause seizures or physical discomfort
- users can avoid content that flashes or distracts them
Keyboard accessibility is a foundational requirement. If a control cannot be reached or activated by keyboard alone, the interface is not fully operable for many users, including people who cannot use a mouse or rely on assistive technology.
Understandable
Information and the operation of the user interface must be understandable.
This includes:
- readable and understandable content
- predictable navigation and behavior
- input assistance and clear error messages
- consistent interaction patterns and labels
If a form unexpectedly changes state after a user enters data, the interaction is harder to understand. Good accessibility often means good UX. Clear labeling, consistent patterns, and explicit validation messages help everyone.
Robust
Content must be robust enough to be interpreted by a wide range of user agents, including assistive technologies.
This means:
- semantically correct markup
- valid HTML and application code
- compatibility with assistive technology
- progressive enhancement for dynamic interfaces
A robust interface is not built on fragile hacks. It uses semantic HTML where possible, then enhances behavior in ways that preserve accessibility.
WCAG levels
WCAG defines conformance levels: A, AA, and AAA.
- Level A is the minimum set of requirements.
- Level AA is what most organizations target as the practical baseline for public websites.
- Level AAA is the highest level and is often not required for entire sites because it can be difficult or impractical for all content.
For example, color contrast requirements are stricter at AAA than AA. A team may target AA because it balances usability with technical practicality. Accessibility work often starts with A and AA because these are the most common legal and business expectations.
Keyboard Interaction and Focus
Keyboard access is not optional. It is a fundamental usability requirement. Many users navigate by Tab, Shift+Tab, arrow keys, Enter, Escape, and Space. The interface must support these patterns and must make the active element visible.
Visible focus
A focus indicator must be obvious. It should not be hidden behind custom styling or disappear because the browser default outline is removed without replacement.
button:focus-visible,
a:focus-visible,
input:focus-visible {
outline: 3px solid #1d4ed8;
outline-offset: 3px;
}This is better than a generic outline: none plus visual styling omitted. Users need to know which element is active. Many teams accidentally remove focus outlines because they look “ugly,” but the result is an inaccessible interface that can still be technically interactive.
Logical tab order
A correct tab order follows the visual and logical order of the page. Tab should move from header controls to navigation, then main content, then forms, then footer. The DOM order should reflect the intended user flow. Avoid moving focus to the end of the document with JavaScript when the user simply navigates a form.
When a component dynamically inserts new content, ensure the keyboard focus moves to meaningful elements with user intent. For example, when a dialog opens, focus should move to the dialog title or first interactive control, not remain on the page behind the overlay.
Keyboard event patterns
Common keyboard patterns include:
TabandShift+Tabfor moving through controlsEnterandSpaceto activate buttons and toggles- arrow keys to move through roving tabindex or menu items
Escapeto close dialogs or dismiss menus
A custom component that behaves like a button must still be keyboard accessible. For example, a custom dropdown built with a div should not just respond to click events. It must support Enter/Space to open it and arrow keys to move within the menu if it acts like a menu or listbox. If it mimics a native control, the behavior should resemble it closely.
Testing keyboard workflow
When you test an interface, try not to use a mouse at all for major flows:
- Tab through the page from the start.
- Check that every focusable element is reached in a sensible order.
- Verify that visible focus is always apparent.
- Trigger major actions with keyboard only.
- Ensure modals and menus can be dismissed with Escape.
- Confirm focus returns to the trigger when dialogs close.
This catches many issues before screen-reader testing begins.
First Rule of ARIA
The first rule of ARIA is simple and important: do not use ARIA to add semantics that are already present in HTML. Many developers reach for role="button" or aria-label when they could use a native element instead. HTML already provides many semantics: button, nav, header, main, form, input, label, and table.
Using native elements comes with built-in behavior, keyboard support, and accessibility handling. ARIA should be used to provide information that cannot be expressed by the native element or to communicate state changes that a browser does not infer.
Examples of good and bad ARIA use
Good:
<button aria-expanded="false" aria-controls="menu">
More options
</button>
<ul id="menu" hidden>
<li>Profile</li>
<li>Settings</li>
</ul>The button is a button. aria-expanded and aria-controls add state and association to help assistive technologies understand the menu relationship.
Bad:
<div onclick="toggleMenu()" role="button">More options</div>This is a generic div with a click listener. It lacks keyboard support and does not provide the built-in semantics of a real button. A native button is the correct choice unless there is a compelling reason not to use it.
Roles, states, and properties
ARIA roles, states, and properties help convey semantic meaning when native HTML is insufficient.
Examples:
aria-expanded: whether a control reveals or hides contentaria-controls: identifies the element controlled by a widgetaria-pressed: toggled state for buttons that behave like switchesaria-live: announces dynamic updates to usersaria-current: indicates the current page, step, or item in a setrole="alert": warns that content is important and should be announced
Use ARIA carefully. Do not over-annotate. A page with too much ARIA can become noisy, and poor state updates can confuse screen readers.
aria-live and announcements
aria-live communicates changes to screen-reader users without requiring them to navigate. Common uses include form validation messages, status updates, toast notifications, and “item added to cart” confirmations.
<div aria-live="polite" id="status" role="status"></div>Then update the status text after a request completes:
const status = document.getElementById('status');
status.textContent = 'Your changes were saved.';Use aria-live="polite" for non-urgent updates and aria-live="assertive" only for critical information. Overuse of assertive live regions can be disruptive.
Contrast, Readability, and Visual Design
Color contrast is a core accessibility issue because low contrast hurts readability for many users, especially people with low vision or color-vision deficiencies. WCAG requires a minimum contrast ratio for body text and large text, and the ratio is stricter at AAA.
Contrast ratios
A contrast ratio compares the luminance of text and background. For normal text, WCAG AA requires at least 4.5:1. Large text needs at least 3:1. The exact ratio depends on font size and weight.
For example:
- dark gray text on a white background may pass or fail depending on the exact shades
- a blue button with white text may be accessible or not, depending on the contrast between the two colors
Use tools and design system tokens to verify contrast, and do not rely on intuition alone. Under accessible design, color should not be the only way to convey meaning. A warning icon or text label helps users who cannot perceive specific colors well.
Typography and readability
Good accessibility also depends on typography:
- text should be large enough to read comfortably
- line length should be appropriate
- headings should create a clear reading structure
- links and controls should be visually distinct
- text should not be justified in a way that creates irregular spacing
Allow users to enlarge text without breaking the layout. Avoid forcing fixed widths that cause clipping or overflow when the browser zoom or text scale is increased.
Accessible Modals and Focus Management
Modal dialogs are a common UI pattern but also a common accessibility failure point. An accessible modal includes a title, a clear close mechanism, focus trapping, keyboard support, and a background that does not allow interaction while the dialog is open.
A simple accessible dialog pattern
<div class="overlay" hidden>
<div
id="dialog"
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
tabindex="-1"
>
<h2 id="dialog-title">Delete item?</h2>
<p>This will permanently remove the item from your account.</p>
<button type="button" data-action="cancel">Cancel</button>
<button type="button" data-action="confirm">Delete</button>
</div>
</div>When the dialog opens, the script should:
- remove
hiddenfrom the overlay - store the previously focused element
- focus the dialog container or first interactive element
- trap Tab within the dialog until it closes
- close on Escape
- restore focus to the original trigger when the dialog closes
Example logic:
const overlay = document.querySelector('.overlay');
const dialog = document.getElementById('dialog');
const previouslyFocused = document.activeElement;
const closeButton = dialog.querySelector('[data-action="cancel"]');
function openDialog() {
overlay.hidden = false;
dialog.focus();
}
function closeDialog() {
overlay.hidden = true;
previouslyFocused?.focus();
}
dialog.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
closeDialog();
}
if (event.key === 'Tab') {
const focusable = [...dialog.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')]
.filter((el) => !el.disabled && !el.closest('[hidden]'));
if (focusable.length === 0) {
event.preventDefault();
return;
}
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
}
});This pattern respects the expectation that focus should remain within the dialog while it is active. It also keeps the page from being interactive behind the modal layer.
Disclosure widgets
Disclosure patterns, such as accordions or expandable sections, must remain accessible too. A disclosure can be built with a button that toggles aria-expanded and controls a region with aria-controls.
<button aria-expanded="false" aria-controls="details-panel" type="button">
Show advanced settings
</button>
<div id="details-panel" hidden>
<p>Additional configuration options.</p>
</div>The JavaScript toggles aria-expanded, updates the hidden state, and ensures the content becomes visible or hidden appropriately. A disclosure is not just a visual reveal; it is an interactive region with semantic communication.
Screen Reader Testing: NVDA and VoiceOver
Automated checks are valuable, but they do not replace real assistive-technology testing. Manual testing with NVDA and VoiceOver helps catch issues that static audits miss, such as incorrect reading order, broken announcements, or confusing focus behavior.
NVDA workflow
NVDA on Windows is a widely used screen reader. A practical test methodology:
- Start at the top of the page with NVDA running.
- Navigate by headings, links, landmarks, and form controls.
- Test the page with only the keyboard.
- Check that interactive elements are announced with appropriate names and states.
- Verify dynamic updates via live regions are announced correctly.
- Ensure modal content is read in the right order and focus is trapped appropriately.
Specific checks:
- Does the page announce the main landmark and navigation landmarks?
- Are button names clear and descriptive?
- Do forms announce field labels correctly?
- Does the dialog announce the title and focus state when opened?
- Is the close button or escape behavior easy to discover?
VoiceOver workflow
VoiceOver on macOS and iOS is common in the Apple ecosystem. Test with Safari and Safari Technology Preview when possible. Typical checks:
- Use Rotor to navigate landmarks, headings, and links.
- Tab through form controls and custom widgets.
- Use VoiceOver commands to review the accessibility tree.
- Confirm live region updates are announced in a useful way.
- Check whether interaction patterns feel predictable and consistent.
What to look for in testing
A screen-reader test is not just a technical pass/fail. It is also a usability check. Ask whether the content and controls are announced naturally and whether a user can complete the task quickly and confidently. If the screen reader says “button” but does not state the action, the accessible name is likely missing. If a modal opens but the focus is lost, the interface fails the expected flow. If a table or form structure is read in the wrong order, the semantics are wrong.
The strongest accessibility process combines multiple methods:
- automated linting and browser accessibility tools
- keyboard-only manual testing
- screen-reader testing with NVDA and VoiceOver
- design review against WCAG requirements
- real user feedback from people with disabilities
Debugging Common Accessibility Problems
Accessibility bugs often come from small assumptions in the implementation. Common issues include:
- removing default focus outlines without replacing them
- relying on color alone to communicate state
- using a generic
divfor a button or aspanfor a link - failing to set labels for inputs and form controls
- adding
aria-*attributes without understanding the underlying semantics - using
display: noneorvisibility: hiddenin ways that remove content from assistive technologies - forgetting to restore focus after a modal closes
A reliable debugging workflow is:
- inspect the DOM and accessible tree in browser devtools
- verify the semantic HTML and ARIA roles state
- keyboard-test the element without mouse interaction
- confirm the correct name, role, and value with a screen reader
- check the browser's accessibility audit or Lighthouse output
This process catches issues before release. It also creates a stronger culture of quality because accessibility is treated as a first-class user experience problem, not as an edge case.
Best Practices for Production Accessibility
A strong production accessibility culture includes these practices:
- build with semantic HTML first
- use native controls before custom widgets
- design visible focus states and keyboard flows at the beginning of the interface design, not as afterthoughts
- verify contrast across themes and states
- test dynamic interfaces with keyboard and assistive technology
- document accessibility expectations in component libraries and patterns
- include accessibility review as part of pull requests and QA checks
Accessibility is not a one-time audit; it is an ongoing responsibility. When a component library is designed well, the team can ship consistent, testable patterns that reduce regressions over time.
Hands-On Exercises
Exercise 1: Create an accessible dialog component
Build a simple dialog with a trigger button, an overlay, and a request to delete an item. The behavior must include focus movement to the dialog, keyboard close with Escape, and focus restoration to the original trigger when the dialog closes.
Solution
Use a native button for the trigger and a dialog container with role="dialog", aria-modal="true", and aria-labelledby referencing the title.
<button id="open-dialog" type="button">Delete item</button>
<div id="dialog-overlay" hidden>
<div id="delete-dialog" role="dialog" aria-modal="true" aria-labelledby="dialog-title" tabindex="-1">
<h2 id="dialog-title">Delete item?</h2>
<p>This removes the item from the list.</p>
<button type="button" class="cancel">Cancel</button>
<button type="button" class="danger">Delete</button>
</div>
</div>And JavaScript:
const openButton = document.getElementById('open-dialog');
const overlay = document.getElementById('dialog-overlay');
const dialog = document.getElementById('delete-dialog');
const cancelButton = dialog.querySelector('.cancel');
let previousFocus = null;
function openDialog() {
previousFocus = document.activeElement;
overlay.hidden = false;
dialog.focus();
}
function closeDialog() {
overlay.hidden = true;
previousFocus?.focus();
}
openButton.addEventListener('click', openDialog);
cancelButton.addEventListener('click', closeDialog);
dialog.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
closeDialog();
}
});This pattern satisfies the key behavior: the user is taken into the dialog, the modal prevents background interaction, and focus returns when dismissed.
Exercise 2: Audit and fix a form with accessibility issues
You are given the following markup:
<label>First name</label>
<input type="text" id="first-name" />
<label>Age</label>
<input type="text" id="age" />
<div onclick="submitForm()">Submit</div>The form is not accessible. Identify the problems and rewrite it.
Solution
Problems include:
- the labels are not associated with their inputs because they are missing the
forattribute or wrapping structure - a
divis used as a submit control, which is not keyboard accessible - there are no focus styles or semantics to signal interaction
- the button lacks a proper accessible label and should probably be a real
button
A corrected version:
<form>
<div>
<label for="first-name">First name</label>
<input id="first-name" name="first-name" type="text" />
</div>
<div>
<label for="age">Age</label>
<input id="age" name="age" type="number" min="0" />
</div>
<button type="submit">Submit</button>
</form>This version uses proper labeling, a native interactive element, and a control that is keyboard operable and announced correctly by assistive technologies.
Summary
Accessibility is about building systems that work for everyone, not just people with obvious visual or physical impairments. WCAG 2.2 and the POUR principles provide a meaningful framework for evaluating whether a product is truly usable. Keyboard support, focus management, semantic markup, well-structured forms, sufficient contrast, and careful ARIA use all matter. Screen-reader testing with NVDA and VoiceOver adds essential evidence that the interface works in real conditions.
Good accessibility is not a separate specialization. It is part of reading comprehension, interaction design, and engineering quality. When teams treat it as foundational work rather than a final check, they create healthier products and a better experience for every user.