Skip to main content

Accessibility Guidelines

Accessibility ensures that applications are usable by people with diverse abilities, including those using assistive technologies like screen readers, keyboard-only navigation, or alternative input devices. Building accessible applications is both a legal requirement in many jurisdictions and a moral imperative to create inclusive digital experiences.

Core Principles

POUR Framework - The foundation of web accessibility as defined by WCAG:

  • Perceivable: Information and UI components must be presentable to users in ways they can perceive (visual, auditory, tactile)
  • Operable: UI components and navigation must be operable by all users, regardless of input method
  • Understandable: Information and operation of the UI must be understandable
  • Robust: Content must be robust enough to work with current and future assistive technologies

Why Accessibility Matters:

  • Approximately 15% of the global population experiences some form of disability
  • Accessible applications often provide better user experiences for everyone (curb-cut effect)
  • Legal compliance with regulations like ADA (Americans with Disabilities Act), Section 508, and European Accessibility Act
  • Improved SEO through semantic HTML and proper structure
  • Better mobile experiences through keyboard navigation and clear focus states

WCAG 2.1 Level AA Compliance

Web Content Accessibility Guidelines (WCAG) 2.1 provides a comprehensive framework for accessibility. Level AA is the standard target for most applications as it addresses the most common accessibility barriers without imposing excessive implementation costs.

Conformance Levels

WCAG defines three levels of conformance, each building upon the previous:

Level A (minimum): Addresses the most severe accessibility barriers that would make content completely inaccessible to some users. Examples include providing text alternatives for images, ensuring keyboard accessibility for all functionality, and avoiding content that could cause seizures.

Level AA (recommended): Addresses broader accessibility barriers and is the legal standard in many jurisdictions. This level includes requirements like sufficient color contrast, visible focus indicators, multiple ways to navigate content, and meaningful link text. Most organizations target this level as it balances accessibility with implementation feasibility.

Level AAA (enhanced): Provides the highest level of accessibility but is not required for entire sites as some content cannot achieve this level. Requirements include enhanced contrast ratios (7:1), sign language interpretation for videos, and extended audio descriptions.

Key Level AA Requirements

The following represents critical Level AA success criteria that impact most applications:

Text Contrast (1.4.3): Normal text must have a contrast ratio of at least 4.5:1, while large text (18pt or 14pt bold) requires 3:1. This ensures text is readable for users with low vision or color blindness. UI components and graphical objects also require 3:1 contrast against adjacent colors (1.4.11).

Resize Text (1.4.4): Users must be able to resize text up to 200% without loss of content or functionality. This accommodates users who need larger text but don't use full screen magnification.

Reflow (1.4.10): Content must reflow to fit in a 320px wide viewport without requiring horizontal scrolling. This helps users who zoom to 400% or use mobile devices.

Keyboard Accessibility (2.1.1, 2.1.2): All functionality must be available via keyboard with no "keyboard traps" where focus cannot be moved away. This serves users who cannot use a mouse due to motor disabilities.

Focus Visible (2.4.7): When an element receives keyboard focus, a visible indicator must be present. Users navigating by keyboard need to see where focus is located.

Link Purpose (2.4.4): The purpose of each link should be clear from its text alone or combined with its context. Avoid generic "click here" or "read more" without context.

Headings and Labels (2.4.6): Headings and labels must describe the topic or purpose of the content they label. This helps all users, especially those using screen readers, understand page structure.

Multiple Ways (2.4.5): Provide more than one way to locate pages (search, sitemap, navigation menu). This accommodates different user preferences and cognitive abilities.

Consistent Navigation (3.2.3): Navigation mechanisms that appear on multiple pages must occur in the same relative order unless the user initiates a change.

Error Identification and Suggestions (3.3.1, 3.3.3): When input errors are detected, identify the error in text and provide suggestions for correction when possible. This helps users with cognitive disabilities or those using screen readers.

See WCAG 2.1 AA Quick Reference for the complete list of criteria.


Semantic HTML and Document Structure

Semantic HTML is the foundation of accessibility. It provides meaning to content, enabling assistive technologies to understand and navigate the page structure effectively. Semantic elements convey both structure and purpose without requiring additional ARIA attributes.

Document Outline and Landmarks

Every page should have a clear hierarchical structure using heading levels and HTML5 landmark elements. This structure allows screen reader users to understand the page organization and jump between sections efficiently.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Payment Dashboard - Acme Bank</title>
</head>
<body>
<!-- Skip link for keyboard users to bypass repetitive navigation -->
<a href="#main-content" class="skip-link">Skip to main content</a>

<!-- Banner landmark: site header with logo and main navigation -->
<header>
<nav aria-label="Main navigation">
<ul>
<li><a href="/dashboard">Dashboard</a></li>
<li><a href="/transfers">Transfers</a></li>
<li><a href="/accounts">Accounts</a></li>
</ul>
</nav>
</header>

<!-- Main landmark: primary page content (only one per page) -->
<main id="main-content">
<!-- Proper heading hierarchy: only one h1, then h2, h3, etc. -->
<h1>Payment Dashboard</h1>

<section aria-labelledby="recent-heading">
<h2 id="recent-heading">Recent Transactions</h2>
<!-- Transaction list content -->
</section>

<section aria-labelledby="pending-heading">
<h2 id="pending-heading">Pending Transfers</h2>
<!-- Pending transfers content -->
</section>
</main>

<!-- Complementary landmark: related information -->
<aside aria-labelledby="notifications-heading">
<h2 id="notifications-heading">Notifications</h2>
<!-- Notification content -->
</aside>

<!-- Contentinfo landmark: site footer -->
<footer>
<nav aria-label="Footer navigation">
<ul>
<li><a href="/privacy">Privacy Policy</a></li>
<li><a href="/terms">Terms of Service</a></li>
<li><a href="/contact">Contact Us</a></li>
</ul>
</nav>
</footer>
</body>
</html>

Language Declaration: The lang attribute on the <html> element tells screen readers which language to use for pronunciation. For multilingual content, use lang attributes on specific elements.

Skip Links: Skip navigation links allow keyboard users to bypass repetitive navigation menus and jump directly to main content. These should be the first focusable element and can be visually hidden until focused.

Landmark Regions: HTML5 semantic elements (<header>, <nav>, <main>, <aside>, <footer>) create ARIA landmark regions automatically. Screen reader users can navigate by landmarks to quickly find content sections. Use aria-label or aria-labelledby to distinguish multiple landmarks of the same type (e.g., main navigation vs. footer navigation).

Heading Hierarchy: Headings (<h1> through <h6>) create a document outline. Use only one <h1> per page for the main title, and nest subsequent headings logically without skipping levels. Screen reader users navigate by headings to understand page structure and find specific sections.

Semantic Elements for Content

Choose HTML elements based on their meaning, not their default styling:

<!-- Use semantic button elements, not divs with click handlers -->
<button type="button" onclick="submitPayment()">Submit Payment</button>
<!-- Avoid: <div onclick="submitPayment()">Submit Payment</div> -->

<!-- Use native links for navigation -->
<a href="/account-details">View Account Details</a>
<!-- Avoid: <span onclick="navigateTo('/account-details')">View Account Details</span> -->

<!-- Use lists for related items -->
<ul>
<li>Transaction 1</li>
<li>Transaction 2</li>
<li>Transaction 3</li>
</ul>

<!-- Use semantic text elements for emphasis -->
<p>Your payment of <strong>$1,234.56</strong> is being processed.</p>
<p><em>Important:</em> This transaction cannot be reversed.</p>

<!-- Use time element for dates and times -->
<p>Transaction completed on <time datetime="2025-01-15T14:30:00Z">January 15, 2025 at 2:30 PM</time></p>

<!-- Use semantic table structure for tabular data -->
<table>
<caption>Recent Transactions</caption>
<thead>
<tr>
<th scope="col">Date</th>
<th scope="col">Description</th>
<th scope="col">Amount</th>
</tr>
</thead>
<tbody>
<tr>
<td>2025-01-15</td>
<td>Coffee Shop</td>
<td>$4.50</td>
</tr>
</tbody>
</table>

Buttons vs. Links: Use <button> for actions that change state or trigger functionality on the current page. Use <a> for navigation to different pages or locations. Buttons respond to Space and Enter keys, while links only respond to Enter.

Strong vs. Bold: Use <strong> for content with strong importance (screen readers may emphasize it), while <b> is for stylistic boldness without semantic importance. Similarly, use <em> for emphasis and <i> for alternative voice or technical terms.

Tables: For data tables, always include <caption> to describe the table's purpose, use <thead>, <tbody>, and <tfoot> to group rows, and use scope attributes on headers to associate them with data cells. For complex tables, use id and headers attributes to explicitly connect cells.

See HTML: A good basis for accessibility for comprehensive coverage of semantic HTML patterns.


ARIA (Accessible Rich Internet Applications)

ARIA provides additional semantics for dynamic content and complex UI patterns that HTML alone cannot express. However, ARIA should be used sparingly - native HTML elements are always preferable when available.

The First Rule of ARIA

If you can use a native HTML element or attribute with the semantics and behavior you require already built in, do so rather than re-purposing an element and adding ARIA roles, states, or properties.

Native elements have built-in keyboard support, focus management, and semantic meaning. ARIA only provides semantics; you must implement all behavior and keyboard interaction manually.

ARIA Roles, States, and Properties

Roles define what an element is or does. Once set, roles should not change:

<!-- Dialog role for modal dialogs -->
<div role="dialog" aria-labelledby="dialog-title" aria-modal="true">
<h2 id="dialog-title">Confirm Transfer</h2>
<p>Are you sure you want to transfer $500?</p>
<button type="button">Confirm</button>
<button type="button">Cancel</button>
</div>

<!-- Tablist role for tab interface -->
<div role="tablist" aria-label="Account views">
<button role="tab" aria-selected="true" aria-controls="summary-panel">Summary</button>
<button role="tab" aria-selected="false" aria-controls="transactions-panel">Transactions</button>
</div>
<div role="tabpanel" id="summary-panel" aria-labelledby="summary-tab">
<!-- Summary content -->
</div>

States and Properties describe the current state or characteristics of an element and change dynamically:

<!-- aria-expanded indicates expandable content state -->
<button aria-expanded="false" aria-controls="filters-panel">
Show Filters
</button>
<div id="filters-panel" hidden>
<!-- Filter controls -->
</div>

<!-- aria-pressed for toggle buttons -->
<button aria-pressed="false" type="button">
<span class="visually-hidden">Mark as favorite</span>

</button>

<!-- aria-current for current page in navigation -->
<nav aria-label="Main navigation">
<a href="/dashboard" aria-current="page">Dashboard</a>
<a href="/transfers">Transfers</a>
<a href="/accounts">Accounts</a>
</nav>

<!-- aria-live for dynamic content updates -->
<div role="status" aria-live="polite" aria-atomic="true">
Transfer completed successfully
</div>

aria-expanded: Indicates whether a collapsible element is expanded (true) or collapsed (false). Must be updated when the element expands or collapses.

aria-pressed: For toggle buttons, indicates the pressed state (true, false, or mixed for tri-state buttons).

aria-current: Indicates the current item in a set (navigation, pagination, etc.). Values include page, step, location, date, time, or true.

aria-live: Announces dynamic content changes to screen readers. Use polite for non-urgent updates (announced after current speech) or assertive for urgent updates (interrupts current speech). Use sparingly to avoid overwhelming users.

aria-atomic: When set to true on a live region, the entire region is announced when any part changes. When false, only the changed portion is announced.

Common ARIA Patterns

Alert Dialog (Modal):

<div role="dialog"
aria-labelledby="alert-title"
aria-describedby="alert-description"
aria-modal="true">
<h2 id="alert-title">Insufficient Funds</h2>
<p id="alert-description">
Your account balance is insufficient for this transfer.
</p>
<button type="button">Close</button>
</div>

When a modal opens, move focus to the first focusable element inside (typically the close button or first action button), trap focus within the modal (prevent tabbing to background content), and restore focus to the triggering element when closed.

Combobox (Autocomplete Search):

<label for="account-search">Search Accounts</label>
<input type="text"
id="account-search"
role="combobox"
aria-autocomplete="list"
aria-expanded="false"
aria-controls="account-listbox"
aria-activedescendant="">

<ul id="account-listbox"
role="listbox"
hidden>
<li role="option" id="option-1">Checking Account (****1234)</li>
<li role="option" id="option-2">Savings Account (****5678)</li>
</ul>

aria-activedescendant: For composite widgets like comboboxes and listboxes where focus remains on the container but a child element is "active," this attribute identifies the active element by its id. Update this as the user arrows through options.

Accordion:

<div class="accordion">
<h3>
<button aria-expanded="false" aria-controls="section1">
Account Details
</button>
</h3>
<div id="section1" hidden>
<!-- Account details content -->
</div>

<h3>
<button aria-expanded="false" aria-controls="section2">
Recent Activity
</button>
</h3>
<div id="section2" hidden>
<!-- Recent activity content -->
</div>
</div>

See ARIA Authoring Practices Guide for complete implementation patterns including required keyboard interactions.

ARIA Labels and Descriptions

Provide accessible names and descriptions for interactive elements:

<!-- aria-label provides a label when visible text isn't suitable -->
<button type="button" aria-label="Close dialog">
×
</button>

<!-- aria-labelledby references an existing element as the label -->
<section aria-labelledby="transactions-heading">
<h2 id="transactions-heading">Recent Transactions</h2>
<!-- Content -->
</section>

<!-- aria-describedby provides additional descriptive information -->
<input type="text"
id="account-number"
aria-describedby="account-help">
<small id="account-help">Enter your 10-digit account number</small>

<!-- Multiple references are space-separated -->
<input type="password"
id="password"
aria-describedby="password-requirements password-strength"
required>
<div id="password-requirements">
Password must be at least 12 characters
</div>
<div id="password-strength" role="status" aria-live="polite">
Password strength: Weak
</div>

aria-label: Provides a string label directly. Use when there's no visible text to reference or when the visible text isn't sufficient (like icon buttons).

aria-labelledby: References one or more elements whose text content forms the label. Overrides aria-label and the native label. Can reference multiple elements to concatenate their text.

aria-describedby: Provides additional descriptive information beyond the label. Used for help text, error messages, or instructions. Screen readers announce this after the label.

For more on ARIA, see the Angular Accessibility Guide and React Accessibility Guide.


Keyboard Navigation

Keyboard accessibility ensures users who cannot use a pointing device can interact with all functionality. This includes users with motor disabilities, blind users, and power users who prefer keyboard navigation.

Tab Order and Focus Management

Tab order follows the DOM order by default. Interactive elements (links, buttons, form inputs) are focusable by default, while non-interactive elements are not.

<!-- Natural tab order -->
<form>
<label for="amount">Amount</label>
<input type="text" id="amount"> <!-- Tab stop 1 -->

<label for="recipient">Recipient</label>
<input type="text" id="recipient"> <!-- Tab stop 2 -->

<button type="submit">Send</button> <!-- Tab stop 3 -->
</form>

<!-- Use tabindex="0" to add non-interactive elements to tab order -->
<div tabindex="0" role="button" onclick="handleClick()">
Custom Button
</div>

<!-- Use tabindex="-1" to remove from tab order but allow programmatic focus -->
<div tabindex="-1" id="error-summary">
<!-- Focus moved here after form submission errors -->
</div>

<!-- Never use positive tabindex values: it creates a confusing tab order -->
<!-- Avoid: <input tabindex="5"> -->

tabindex="0": Adds the element to the natural tab order. The element will be focusable in the order it appears in the DOM. Use for custom interactive elements that don't have native keyboard support.

tabindex="-1": Removes the element from the tab order but allows programmatic focus via JavaScript (e.g., element.focus()). Useful for headings you want to focus when skipping to sections, or for managing focus in widgets like tab panels.

Positive tabindex: Avoid entirely. It creates a confusing tab order where elements with positive values are focused first (in numeric order), then elements with tabindex="0" in DOM order. This makes the tab order unpredictable and difficult to maintain.

Focus Indicators

Visible focus indicators show keyboard users where focus is located. Never remove focus outlines without providing an alternative.

/* Avoid removing default focus outline without replacement */
button:focus {
outline: none; /* Problematic without alternative */
}

/* Provide enhanced focus indicator */
button:focus-visible {
outline: 3px solid #0066cc;
outline-offset: 2px;
border-radius: 4px;
}

/* Use :focus-visible to show indicator only for keyboard focus */
.custom-button:focus-visible {
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.5);
}

/* Ensure focus indicator has sufficient contrast */
a:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
}

:focus vs. :focus-visible: The :focus pseudo-class applies when an element receives focus via any method (mouse click, keyboard, programmatic). The :focus-visible pseudo-class applies only when the browser determines the focus indicator should be visible (typically keyboard focus but not mouse clicks). Use :focus-visible to avoid distracting outlines on mouse clicks while maintaining keyboard accessibility.

Focus indicators must have a contrast ratio of at least 3:1 against the adjacent background colors (WCAG 2.4.11). Make the outline thick enough to be visible (at least 2px).

Keyboard Interaction Patterns

Different UI components require specific keyboard interactions:

Buttons: Activated with Space or Enter. If the button opens a menu, use Arrow keys to navigate menu items.

Links: Activated with Enter only (not Space, which scrolls the page).

Checkboxes and Radio Buttons: Space toggles checkboxes. Arrow keys navigate between radio buttons in a group and automatically select the focused option.

Text Inputs: Standard text editing keys (arrows, Home, End, Delete, Backspace). Tab moves to next field.

Select Dropdowns: Space or Enter opens the dropdown, Arrow keys navigate options, Enter selects, Escape closes without selecting.

Custom Widgets (tabs, accordions, dialogs): Must implement ARIA Authoring Practices keyboard patterns.

Example tab implementation:

// Tab keyboard interaction (React example)
function TabList({ tabs, activeTab, onTabChange }) {
const handleKeyDown = (event: React.KeyboardEvent, index: number) => {
const tabCount = tabs.length;
let newIndex = index;

// Arrow key navigation
if (event.key === 'ArrowRight') {
newIndex = (index + 1) % tabCount; // Wrap to first tab
} else if (event.key === 'ArrowLeft') {
newIndex = (index - 1 + tabCount) % tabCount; // Wrap to last tab
} else if (event.key === 'Home') {
newIndex = 0;
} else if (event.key === 'End') {
newIndex = tabCount - 1;
} else {
return; // Not a handled key
}

event.preventDefault();
onTabChange(newIndex);
// Focus the newly selected tab
document.getElementById(`tab-${newIndex}`)?.focus();
};

return (
<div role="tablist" aria-label="Account views">
{tabs.map((tab, index) => (
<button
key={tab.id}
id={`tab-${index}`}
role="tab"
aria-selected={activeTab === index}
aria-controls={`panel-${index}`}
tabIndex={activeTab === index ? 0 : -1}
onClick={() => onTabChange(index)}
onKeyDown={(e) => handleKeyDown(e, index)}
>
{tab.label}
</button>
))}
</div>
);
}

Focus Management in Tabs: Only the active tab is in the tab order (tabindex="0"), while inactive tabs are removed from tab order (tabindex="-1"). Arrow keys move focus between tabs and activate them, while Tab moves focus out of the tab list to the tab panel.

Focus Trapping

Modal dialogs and overlays must trap focus to prevent keyboard users from tabbing into background content:

// Focus trap implementation (React example)
function Modal({ isOpen, onClose, children }) {
const modalRef = useRef<HTMLDivElement>(null);
const previouslyFocusedElement = useRef<HTMLElement | null>(null);

useEffect(() => {
if (!isOpen) return;

// Store previously focused element
previouslyFocusedElement.current = document.activeElement as HTMLElement;

// Move focus to modal
const modal = modalRef.current;
if (modal) {
modal.focus();
}

// Return focus when modal closes
return () => {
previouslyFocusedElement.current?.focus();
};
}, [isOpen]);

const handleKeyDown = (event: React.KeyboardEvent) => {
if (event.key === 'Escape') {
onClose();
return;
}

// Trap Tab key
if (event.key === 'Tab') {
const modal = modalRef.current;
if (!modal) return;

const focusableElements = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0] as HTMLElement;
const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;

if (event.shiftKey && document.activeElement === firstElement) {
// Shift+Tab on first element: focus last
event.preventDefault();
lastElement?.focus();
} else if (!event.shiftKey && document.activeElement === lastElement) {
// Tab on last element: focus first
event.preventDefault();
firstElement?.focus();
}
}
};

if (!isOpen) return null;

return (
<div
ref={modalRef}
role="dialog"
aria-modal="true"
tabIndex={-1}
onKeyDown={handleKeyDown}
>
{children}
</div>
);
}

Focus Restoration: When a modal closes, focus must return to the element that opened it. Store a reference to document.activeElement before opening the modal and restore focus to it on close.

For keyboard navigation in Angular, see Angular Accessibility. For React, see React Accessibility.


Color Contrast and Visual Design

Color alone cannot convey information, and all text must have sufficient contrast against its background. These requirements ensure content is perceivable by users with low vision, color blindness, or viewing in bright sunlight.

Contrast Ratios

WCAG 2.1 Level AA requires:

  • 4.5:1 minimum for normal text (less than 18pt or 14pt bold)
  • 3:1 minimum for large text (18pt and larger, or 14pt bold and larger)
  • 3:1 minimum for UI components and graphical objects (icons, form borders, charts)
/* Good: Sufficient contrast for normal text */
.text-primary {
color: #1a1a1a; /* Dark gray */
background-color: #ffffff; /* White */
/* Contrast ratio: 16.1:1 ✓ */
}

/* Good: Sufficient contrast for large headings */
.heading-large {
color: #595959; /* Medium gray */
background-color: #ffffff;
font-size: 24px;
/* Contrast ratio: 6.5:1 ✓ */
}

/* Problematic: Insufficient contrast for normal text */
.text-light {
color: #999999; /* Light gray */
background-color: #ffffff;
/* Contrast ratio: 2.8:1 ✗ Fails WCAG AA */
}

/* Good: Sufficient contrast for interactive elements */
.button-primary {
background-color: #0066cc; /* Primary blue */
color: #ffffff; /* White text */
border: 2px solid #004c99; /* Darker border for definition */
/* Text contrast: 8.6:1 ✓ */
/* Border contrast: 5.9:1 ✓ */
}

/* Ensure focus indicators have adequate contrast */
.button-primary:focus-visible {
outline: 3px solid #ffcc00; /* Yellow outline */
outline-offset: 2px;
/* Outline contrasts with both button and page background ✓ */
}

Testing Contrast: Use browser DevTools contrast checkers, the WebAIM Contrast Checker, or design tools like Figma (built-in contrast checker). Test against both light and dark mode backgrounds.

UI Component Contrast: Form inputs, buttons, and icons must have 3:1 contrast against adjacent colors. This includes the border of a text input against the background, icon colors against their background, and disabled states (though disabled elements are exempt from contrast requirements).

Color Independence

Never use color as the only means to convey information. Always provide additional visual cues:

<!-- Bad: Color alone indicates status -->
<span style="color: red;">Error</span>
<span style="color: green;">Success</span>

<!-- Good: Icon + color + text -->
<span class="status-error">
<svg aria-hidden="true" class="icon-error">...</svg>
<span>Error: Payment failed</span>
</span>

<span class="status-success">
<svg aria-hidden="true" class="icon-check">...</svg>
<span>Success: Payment completed</span>
</span>

<!-- Good: Form validation with multiple indicators -->
<div class="form-group error">
<label for="account-number">Account Number</label>
<input
type="text"
id="account-number"
aria-invalid="true"
aria-describedby="account-error"
class="input-error">
<!-- Error indicated by: red border, icon, error text, aria-invalid -->
<svg aria-hidden="true" class="icon-error">...</svg>
<span id="account-error" class="error-message">
Account number must be 10 digits
</span>
</div>

Charts and Graphs: Use patterns, labels, or icons in addition to color:

// Chart with accessible color scheme and patterns
const chartConfig = {
datasets: [
{
label: 'Income',
data: incomeData,
backgroundColor: '#0066cc',
borderColor: '#004c99',
borderWidth: 2,
borderDash: [], // Solid line
},
{
label: 'Expenses',
data: expenseData,
backgroundColor: '#cc0000',
borderColor: '#990000',
borderWidth: 2,
borderDash: [5, 5], // Dashed line for distinction
}
],
options: {
// Ensure tooltips and data labels are accessible
plugins: {
tooltip: {
enabled: true,
backgroundColor: '#1a1a1a',
titleColor: '#ffffff',
bodyColor: '#ffffff',
// High contrast tooltip
},
legend: {
labels: {
// Add pattern indicators to legend
generateLabels: (chart) => {
// Custom legend with patterns
}
}
}
}
}
};

For data visualizations, provide data tables as an alternative or supplement to charts. See Data Visualization Accessibility for comprehensive guidance.

Dark Mode and Theme Considerations

When implementing dark mode, maintain contrast ratios and ensure interactive elements remain distinguishable:

/* Light theme */
:root {
--color-text-primary: #1a1a1a;
--color-background: #ffffff;
--color-surface: #f5f5f5;
--color-border: #d0d0d0;
--color-link: #0066cc;
--color-link-visited: #551a8b;
}

/* Dark theme */
[data-theme="dark"] {
--color-text-primary: #e0e0e0;
--color-background: #1a1a1a;
--color-surface: #2d2d2d;
--color-border: #404040;
--color-link: #66b3ff; /* Lighter blue for dark backgrounds */
--color-link-visited: #b18cd9; /* Lighter purple */
}

/* Apply theme variables */
body {
color: var(--color-text-primary);
background-color: var(--color-background);
}

a {
color: var(--color-link);
}

a:visited {
color: var(--color-link-visited);
}

Test both themes with contrast checkers to ensure all states (normal, hover, focus, disabled) meet requirements.


Accessible Forms

Forms are critical to most applications and present unique accessibility challenges. Accessible forms require proper labeling, clear error messaging, and appropriate input types.

Form Labels and Instructions

Every form input must have an associated label. Labels should be visible and positioned before (above or to the left of) the input:

<!-- Good: Explicit label association -->
<label for="email">Email Address</label>
<input type="email" id="email" name="email" required>

<!-- Good: Nested label (implicit association) -->
<label>
Email Address
<input type="email" name="email" required>
</label>

<!-- Bad: Placeholder as label (not accessible) -->
<input type="email" placeholder="Email Address" required>

<!-- Good: Instructions and help text -->
<label for="password">Password</label>
<input
type="password"
id="password"
aria-describedby="password-requirements"
required>
<small id="password-requirements">
Must be at least 12 characters and include uppercase, lowercase, number, and symbol
</small>

<!-- Good: Required field indication -->
<label for="account-number">
Account Number
<abbr title="required" aria-label="required">*</abbr>
</label>
<input type="text" id="account-number" required>

Placeholder vs. Label: Placeholders disappear when typing begins, making them unsuitable as labels. They have poor contrast (often too light), and screen readers may not announce them consistently. Always use a visible <label> element.

Required Field Indicators: Indicate required fields with visual markers (asterisks, "required" text) and the required attribute. Explain the convention at the top of the form: "Fields marked with * are required."

Help Text: Associate help text with inputs using aria-describedby. Screen readers will announce the help text after the label when the field receives focus.

Input Types and Validation

Use appropriate HTML5 input types to provide better mobile keyboards and built-in validation:

<!-- Use semantic input types -->
<label for="email">Email</label>
<input type="email" id="email" autocomplete="email">

<label for="phone">Phone Number</label>
<input type="tel" id="phone" autocomplete="tel">

<label for="dob">Date of Birth</label>
<input type="date" id="dob" autocomplete="bday">

<label for="amount">Transfer Amount</label>
<input type="number" id="amount" min="0.01" step="0.01">

<!-- Use autocomplete for common fields -->
<label for="name">Full Name</label>
<input type="text" id="name" autocomplete="name">

<label for="address">Address</label>
<input type="text" id="address" autocomplete="street-address">

Autocomplete Attributes: Use HTML autocomplete values to enable browser autofill and password managers. This reduces cognitive load and improves accuracy for users with cognitive disabilities.

Error Handling and Validation

Error messages must be clear, specific, and programmatically associated with the invalid field:

<!-- Good: Inline error with aria-describedby -->
<div class="form-group">
<label for="routing-number">Routing Number</label>
<input
type="text"
id="routing-number"
aria-invalid="true"
aria-describedby="routing-error">
<span id="routing-error" class="error-message" role="alert">
Routing number must be exactly 9 digits
</span>
</div>

<!-- Good: Error summary at top of form -->
<div role="alert" class="error-summary" tabindex="-1" id="error-summary">
<h2>There are 3 errors in this form:</h2>
<ul>
<li><a href="#routing-number">Routing number must be 9 digits</a></li>
<li><a href="#account-number">Account number is required</a></li>
<li><a href="#amount">Amount must be greater than $0</a></li>
</ul>
</div>

<script>
// Focus error summary after validation
function validateForm(form) {
const errors = findErrors(form);
if (errors.length > 0) {
displayErrorSummary(errors);
document.getElementById('error-summary')?.focus();
return false;
}
return true;
}
</script>

aria-invalid: Set to "true" when a field contains an invalid value. Screen readers announce this state when the field receives focus. Set to "false" or remove the attribute when the error is corrected.

Error Summary: When a form submission fails validation, display an error summary at the top of the form and move focus to it. The summary should list all errors with links to the corresponding fields. Use role="alert" or an ARIA live region so screen readers announce the summary.

Inline Errors: Display error messages adjacent to the field (typically below) and associate them using aria-describedby. Use role="alert" on the error message so it's announced immediately when it appears.

Success Messages: Use ARIA live regions to announce success messages:

<div role="status" aria-live="polite" class="success-message">
<svg aria-hidden="true" class="icon-check">...</svg>
Payment submitted successfully
</div>

For framework-specific form accessibility, see Angular Forms and React Forms.


Screen Reader Testing

Screen readers are assistive technologies that read page content aloud and provide navigation shortcuts. Testing with screen readers is essential to verify that semantic HTML, ARIA, and keyboard interactions work correctly.

NVDA (NonVisual Desktop Access): Free, open-source screen reader for Windows. Most commonly used globally and recommended for testing.

JAWS (Job Access With Speech): Commercial screen reader for Windows. Widely used in enterprise and government contexts. Expensive but offers advanced features.

VoiceOver: Built into macOS and iOS. Included with all Apple devices at no additional cost. Most convenient for Mac users.

TalkBack: Built into Android. The primary screen reader for Android devices.

Narrator: Built into Windows. Basic screen reader included with Windows 10 and 11.

Basic Screen Reader Commands

Understanding basic screen reader navigation helps you test effectively:

NVDA (Windows):

  • Start/Stop: Ctrl + Alt + N
  • Read next item: Down Arrow
  • Read previous item: Up Arrow
  • Navigate headings: H (next), Shift + H (previous)
  • Navigate landmarks: D (next), Shift + D (previous)
  • Navigate links: K (next), Shift + K (previous)
  • Navigate form fields: F (next), Shift + F (previous)
  • Read entire page: Ctrl + Down Arrow
  • Toggle forms mode: Insert + Space

VoiceOver (macOS):

  • Start/Stop: Cmd + F5
  • Move to next item: Ctrl + Option + Right Arrow
  • Move to previous item: Ctrl + Option + Left Arrow
  • Open rotor: Ctrl + Option + U (then arrow keys to navigate by headings, landmarks, links, etc.)
  • Read entire page: Ctrl + Option + A

VoiceOver (iOS):

  • Enable: Settings > Accessibility > VoiceOver
  • Move to next item: Swipe right
  • Move to previous item: Swipe left
  • Activate item: Double-tap
  • Open rotor: Rotate two fingers on screen

Testing Checklist

When testing with a screen reader, verify:

  1. Page Title: The page title is announced when the page loads and accurately describes the page content.

  2. Landmarks and Structure: Can you navigate by landmarks (banner, navigation, main, contentinfo) and headings to quickly find sections?

  3. Links and Buttons: Are link purposes clear from their text alone? Are buttons clearly identified as buttons?

  4. Forms: Are all labels announced? Are required fields indicated? Are error messages announced and associated with the correct field?

  5. Images: Do images have appropriate alt text? Are decorative images hidden with alt="" or aria-hidden="true"?

  6. Dynamic Content: Are live regions announcing changes appropriately? Are status messages announced?

  7. Dialogs and Modals: When a modal opens, is focus moved to it? Is the modal content announced correctly? Can you trap focus and navigate within it?

  8. Tables: Do data tables announce headers for each cell? Is the table purpose clear from the caption?

  9. Tab Order: Does the tab order follow a logical sequence? Are there any keyboard traps?

  10. Skip Links: Can you skip repetitive navigation and jump directly to main content?

Testing Best Practices

Turn Off Your Monitor: Try navigating the application with your monitor turned off (or eyes closed). This simulates the screen reader user experience more accurately and reveals issues you might miss visually.

Test Real-World Tasks: Don't just navigate the page structure. Complete actual tasks like submitting a form, filtering a list, or completing a multi-step workflow.

Test on Multiple Screen Readers: Different screen readers have different behaviors and bugs. Test on at least NVDA (Windows) and VoiceOver (macOS).

Document Issues: Record which elements are problematic, what the screen reader announces, and what it should announce instead.

For detailed screen reader testing guides, see WebAIM Screen Reader Testing and Testing with Screen Readers.


Accessibility Testing Tools

Automated testing tools can identify many accessibility issues quickly, but they cannot catch all problems. Manual testing and screen reader testing are still essential.

Browser Extensions

axe DevTools (Chrome, Firefox, Edge): The most comprehensive automated accessibility testing tool. Identifies WCAG violations, provides clear explanations, and suggests fixes. The free version covers most needs; paid version adds advanced features.

WAVE (Web Accessibility Evaluation Tool): Visualizes accessibility issues directly on the page with icons and overlays. Excellent for understanding the page structure and finding issues in context.

Lighthouse (Chrome DevTools): Built into Chrome DevTools, includes accessibility audit as part of overall quality assessment. Provides scores and specific recommendations.

Microsoft Accessibility Insights: Provides automated checks and guided manual tests. Includes a "tab stop visualizer" showing tab order and a "headings map" showing document structure.

Automated Testing in CI/CD

Integrate accessibility testing into your CI/CD pipeline to catch regressions:

axe-core (JavaScript library): The engine behind axe DevTools, can be integrated into unit tests:

// Jest + React Testing Library + axe-core
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';

expect.extend(toHaveNoViolations);

test('PaymentForm should have no accessibility violations', async () => {
const { container } = render(<PaymentForm />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});

Pa11y (Command-line tool): Runs accessibility tests against URLs:

# Test a single page
pa11y https://example.com/payment

# Test multiple pages
pa11y https://example.com/payment https://example.com/dashboard

# Generate HTML report
pa11y-ci --reporter html --report-dir ./accessibility-reports

Cypress with axe-core:

// Cypress accessibility test
describe('Payment Dashboard', () => {
beforeEach(() => {
cy.visit('/dashboard');
cy.injectAxe(); // Inject axe-core
});

it('should have no accessibility violations', () => {
cy.checkA11y(); // Run axe checks
});

it('should have no violations after interaction', () => {
cy.get('#show-filters-button').click();
cy.checkA11y(); // Check expanded state
});
});

Playwright with axe-core:

// Playwright accessibility test
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test('Payment dashboard should be accessible', async ({ page }) => {
await page.goto('/dashboard');

const accessibilityScanResults = await new AxeBuilder({ page })
.analyze();

expect(accessibilityScanResults.violations).toEqual([]);
});

Manual Testing Checklist

Automated tools catch approximately 30-40% of accessibility issues. Manual testing is required for:

  • Keyboard Navigation: Can you access all functionality with keyboard only? Is the tab order logical? Are there visible focus indicators?
  • Screen Reader Compatibility: Does content make sense when read aloud? Are interactive elements properly announced?
  • Zoom and Reflow: Can you zoom to 200% without content overlapping or disappearing? Does content reflow properly on narrow viewports?
  • Color Contrast: Do all text and UI elements meet contrast requirements in all states (normal, hover, focus, disabled)?
  • Dynamic Content: Are changes announced to screen readers? Do live regions work correctly?
  • Error Messages: Are errors clearly identified and associated with the correct fields?
  • Alternative Text: Do images have appropriate alt text that conveys their meaning?

For a comprehensive manual testing process, see Manual Accessibility Testing from W3C.


Mobile Accessibility

Mobile accessibility extends web accessibility principles to native mobile platforms with platform-specific considerations.

iOS Accessibility (VoiceOver)

Use UIKit or SwiftUI accessibility APIs:

// UIKit accessibility
let submitButton = UIButton()
submitButton.accessibilityLabel = "Submit Payment" // Overrides default label
submitButton.accessibilityHint = "Transfers money to the selected account" // Additional context
submitButton.accessibilityTraits = .button // Announces "Submit Payment, button"

// Hide decorative elements
decorativeImageView.isAccessibilityElement = false

// Group related elements
let balanceContainer = UIView()
balanceContainer.isAccessibilityElement = true
balanceContainer.accessibilityLabel = "Account balance $1,234.56 available"

// SwiftUI accessibility
Button("Submit Payment") {
submitPayment()
}
.accessibilityLabel("Submit Payment")
.accessibilityHint("Transfers money to the selected account")

Image(decorative: "background-pattern") // Automatically hidden from VoiceOver

Text("$1,234.56")
.accessibilityLabel("One thousand two hundred thirty four dollars and fifty six cents")

Dynamic Type (Text Scaling): Support Dynamic Type to allow users to adjust text size:

// UIKit
label.font = UIFont.preferredFont(forTextStyle: .body)
label.adjustsFontForContentSizeCategory = true

// SwiftUI
Text("Account balance")
.font(.body) // Automatically scales with Dynamic Type

VoiceOver Rotor: Provide custom rotor actions for navigation:

// Custom rotor for transactions
let transactionRotor = UIAccessibilityCustomRotor(name: "Transactions") { predicate in
// Return next/previous transaction based on predicate.searchDirection
}
view.accessibilityCustomRotors = [transactionRotor]

See iOS Accessibility Guidelines for comprehensive iOS accessibility patterns.

Android Accessibility (TalkBack)

Use Android accessibility APIs:

// Set content descriptions
submitButton.contentDescription = "Submit Payment"

// Hide decorative elements
decorativeImage.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO

// Group related content
balanceContainer.apply {
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
contentDescription = "Account balance $1,234.56 available"
}

// Announce changes for dynamic content
LiveRegionCompat.setLiveRegion(
statusText,
ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE
)

// Custom accessibility actions
ViewCompat.addAccessibilityAction(
transactionRow,
"View Details"
) { view, arguments ->
viewTransactionDetails()
true
}

Text Scaling: Support user-defined text sizes:

<!-- Use scalable units (sp) for text -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:text="Account balance" />

Touch Target Sizes: Ensure interactive elements are at least 48x48dp (approximately 9mm):

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="48dp"
android:minHeight="48dp"
android:text="Submit" />

See Android Accessibility Guidelines for comprehensive Android accessibility patterns.

React Native Accessibility

React Native provides cross-platform accessibility props:

// Basic accessibility props
<TouchableOpacity
accessible={true}
accessibilityLabel="Submit Payment"
accessibilityHint="Transfers money to the selected account"
accessibilityRole="button"
onPress={submitPayment}
>
<Text>Submit</Text>
</TouchableOpacity>

// Hide decorative elements
<Image
source={require('./decorative-pattern.png')}
accessibilityElementsHidden={true} // iOS
importantForAccessibility="no" // Android
/>

// Group related elements
<View
accessible={true}
accessibilityLabel="Account balance $1,234.56 available"
>
<Text>Balance:</Text>
<Text>$1,234.56</Text>
</View>

// Announce dynamic changes
import { AccessibilityInfo } from 'react-native';

function announceSuccess() {
AccessibilityInfo.announceForAccessibility('Payment completed successfully');
}

Screen Reader Detection: Adjust UI based on screen reader state:

import { AccessibilityInfo } from 'react-native';

function PaymentForm() {
const [screenReaderEnabled, setScreenReaderEnabled] = useState(false);

useEffect(() => {
// Check if screen reader is enabled
AccessibilityInfo.isScreenReaderEnabled().then(setScreenReaderEnabled);

// Listen for changes
const subscription = AccessibilityInfo.addEventListener(
'screenReaderChanged',
setScreenReaderEnabled
);

return () => subscription.remove();
}, []);

return (
<View>
{/* Provide additional context for screen reader users */}
{screenReaderEnabled && (
<Text accessibilityLiveRegion="polite">
Use the "Pay" button to submit this transfer
</Text>
)}
</View>
);
}

See React Native Accessibility for comprehensive React Native accessibility patterns.


Accessibility in Modern Frameworks

Angular Accessibility

Angular provides built-in accessibility support through the CDK (Component Dev Kit):

import { A11yModule } from '@angular/cdk/a11y';

// Focus trap directive
<div cdkTrapFocus cdkTrapFocusAutoCapture>
<h2>Confirm Transfer</h2>
<button (click)="confirm()">Confirm</button>
<button (click)="cancel()">Cancel</button>
</div>

// Live announcer service
import { LiveAnnouncer } from '@angular/cdk/a11y';

export class PaymentComponent {
constructor(private liveAnnouncer: LiveAnnouncer) {}

submitPayment() {
// Submit payment logic
this.liveAnnouncer.announce('Payment submitted successfully', 'polite');
}
}

// Focus monitor
import { FocusMonitor } from '@angular/cdk/a11y';

export class CustomButton implements OnInit, OnDestroy {
constructor(
private elementRef: ElementRef,
private focusMonitor: FocusMonitor
) {}

ngOnInit() {
this.focusMonitor.monitor(this.elementRef, true);
}

ngOnDestroy() {
this.focusMonitor.stopMonitoring(this.elementRef);
}
}

See Angular Accessibility Guide for framework-specific patterns and testing strategies.

React Accessibility

React supports ARIA attributes directly in JSX:

// ARIA props in React
function PaymentDialog({ isOpen, onClose }) {
return isOpen ? (
<div
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
aria-describedby="dialog-description"
>
<h2 id="dialog-title">Confirm Payment</h2>
<p id="dialog-description">
Transfer $500 to checking account?
</p>
<button onClick={onClose}>Confirm</button>
<button onClick={onClose}>Cancel</button>
</div>
) : null;
}

// Use refs for focus management
function SearchInput() {
const inputRef = useRef<HTMLInputElement>(null);

useEffect(() => {
// Focus input on mount
inputRef.current?.focus();
}, []);

return (
<input
ref={inputRef}
type="search"
aria-label="Search transactions"
/>
);
}

React Testing Library encourages accessible selectors:

import { render, screen } from '@testing-library/react';

test('PaymentForm renders correctly', () => {
render(<PaymentForm />);

// Query by role (accessible selector)
const submitButton = screen.getByRole('button', { name: /submit/i });

// Query by label
const amountInput = screen.getByLabelText('Amount');

expect(submitButton).toBeInTheDocument();
expect(amountInput).toBeInTheDocument();
});

See React Accessibility Guide for comprehensive React accessibility patterns.


Common Accessibility Anti-Patterns

Avoid these common mistakes that harm accessibility:

Anti-Pattern 1: Removing Focus Outlines

/* Bad: Removes focus indicator */
*:focus {
outline: none;
}

/* Good: Provide alternative focus indicator */
*:focus-visible {
outline: 3px solid #0066cc;
outline-offset: 2px;
}

Removing focus outlines makes keyboard navigation impossible for sighted keyboard users.

Anti-Pattern 2: Using Divs and Spans as Buttons

<!-- Bad: Non-semantic clickable div -->
<div onclick="submitPayment()">Submit Payment</div>

<!-- Good: Semantic button element -->
<button type="button" onclick="submitPayment()">Submit Payment</button>

Divs and spans are not keyboard accessible, not announced as interactive elements by screen readers, and don't provide native button behaviors (Space/Enter activation, form submission).

Anti-Pattern 3: Auto-Playing Media

<!-- Bad: Auto-playing video with sound -->
<video autoplay>
<source src="promo.mp4" type="video/mp4">
</video>

<!-- Good: User-controlled playback -->
<video controls>
<source src="promo.mp4" type="video/mp4">
<track kind="captions" src="captions.vtt" srclang="en" label="English">
</video>

Auto-playing content with sound disorients screen reader users and can trigger seizures in users with photosensitive epilepsy. Always provide user controls and captions.

Anti-Pattern 4: Placeholder as Label

<!-- Bad: Placeholder as only label -->
<input type="text" placeholder="Email address">

<!-- Good: Visible label -->
<label for="email">Email Address</label>
<input type="text" id="email">

Placeholders disappear when typing, have insufficient contrast, and are not consistently announced by screen readers.

Anti-Pattern 5: Opening New Windows Without Warning

<!-- Bad: Opens new window unexpectedly -->
<a href="https://example.com">Terms of Service</a>

<!-- Good: Indicates new window -->
<a href="https://example.com" target="_blank" rel="noopener noreferrer">
Terms of Service
<span class="visually-hidden">(opens in new window)</span>
</a>

Unexpectedly opening new windows disorients users, especially those using screen readers or with cognitive disabilities.

Anti-Pattern 6: Time Limits Without Controls

// Bad: Hard timeout without warning or extension
setTimeout(() => {
logout();
}, 300000); // 5 minutes

// Good: Warning with option to extend
function showTimeoutWarning() {
const warningDialog = showDialog({
title: 'Session Expiring',
message: 'Your session will expire in 1 minute. Would you like to continue?',
actions: [
{ label: 'Continue Session', onClick: () => extendSession() },
{ label: 'Log Out', onClick: () => logout() }
]
});

// Move focus to dialog and announce to screen readers
warningDialog.focus();
}

Time limits disadvantage users who need more time to read or interact, including users with cognitive or motor disabilities. Provide warnings and extension options.


Further Resources

Specifications and Guidelines

Testing and Validation

Learning Resources

Framework-Specific Resources