WCAG AA Checklist for Web Apps: The Complete Developer Reference
WCAG AA Checklist for Web Applications
WCAG (Web Content Accessibility Guidelines) is the international standard for web accessibility. Level AA is the baseline that most legal requirements, enterprise procurement processes, and accessibility policies reference. If someone says "our product is accessible," they usually mean WCAG 2.1 AA.
This checklist covers every AA criterion that applies to web applications, organized by the four WCAG principles: Perceivable, Operable, Understandable, and Robust. Each item includes what the criterion requires, why it matters, and how to implement it in a React or Next.js application.
Use this as a reference during development, code review, and QA. It's not a substitute for testing with real assistive technology users, but it covers the technical requirements your code needs to meet.
Principle 1: Perceivable
Users must be able to perceive the information being presented. It can't be invisible to all of their senses.
1.1 Text Alternatives
1.1.1 Non-text Content (Level A)
Every non-text element needs a text alternative that serves the same purpose.
- All
<img>elements have meaningfulalttext that describes the image's purpose, not its appearance - Decorative images use
alt=""(empty alt) so screen readers skip them - Icon buttons have
aria-labelor visually hidden text describing the action - SVG icons used as content have
role="img"and anaria-label - Charts and data visualizations have text descriptions or accessible data tables as alternatives
- CAPTCHAs provide an alternative method (audio, logic-based)
// Good: Meaningful alt text
<img src="/dashboard-chart.png" alt="Monthly revenue chart showing 23% growth from January to March 2026" />
// Good: Decorative image
<img src="/decorative-wave.svg" alt="" />
// Good: Icon button with label
<button aria-label="Close dialog">
<XIcon aria-hidden="true" />
</button>
1.2 Time-Based Media
1.2.1 Audio-only and Video-only (Level A)
- Pre-recorded audio content has a text transcript
- Pre-recorded video-only content has a text description or audio track
1.2.2 Captions (Level A)
- Pre-recorded video with audio has synchronized captions
1.2.4 Captions (Live) (Level AA)
- Live video with audio has real-time captions
1.2.5 Audio Description (Level AA)
- Pre-recorded video has audio description for visual content not described in the main audio track
1.3 Adaptable
1.3.1 Info and Relationships (Level A)
Structure and relationships conveyed visually must also be conveyed programmatically.
- Headings use proper
<h1>through<h6>elements in logical order (no skipping levels) - Lists use
<ul>,<ol>, or<dl>elements, not styled<div>s - Data tables use
<table>,<thead>,<th>, and<td>with properscopeattributes - Form inputs are associated with labels using
<label htmlFor="id">oraria-labelledby - Related form fields are grouped with
<fieldset>and<legend> - Landmarks are used:
<nav>,<main>,<aside>,<header>,<footer>
// Good: Proper table structure
<table>
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Role</th>
<th scope="col">Status</th>
</tr>
</thead>
<tbody>
<tr>
<td>Jane Smith</td>
<td>Admin</td>
<td>Active</td>
</tr>
</tbody>
</table>
1.3.2 Meaningful Sequence (Level A)
- The DOM order matches the visual reading order
- CSS does not rearrange content in a way that changes meaning (be careful with
order,flex-direction: row-reverse, andgridreordering)
1.3.3 Sensory Characteristics (Level A)
- Instructions don't rely solely on shape, size, visual location, or sound ("Click the round button" or "The option on the right")
- Error messages identify the field by name, not just by color or position
1.3.4 Orientation (Level AA)
- The app works in both portrait and landscape orientation
- No content or functionality is restricted to a single orientation unless essential
1.3.5 Identify Input Purpose (Level AA)
- Form inputs for personal data (name, email, phone, address) use the appropriate
autocompleteattribute
<input type="email" name="email" autoComplete="email" />
<input type="text" name="name" autoComplete="name" />
<input type="tel" name="phone" autoComplete="tel" />
1.4 Distinguishable
1.4.1 Use of Color (Level A)
- Color is not the only way to convey information (error states, status indicators, chart data)
- Links within text are distinguishable by more than just color (underline, bold, or icon)
- Form error states use an icon or text label in addition to a red border
1.4.2 Audio Control (Level A)
- Any audio that plays automatically for more than 3 seconds can be paused, stopped, or volume-controlled
1.4.3 Contrast (Minimum) (Level AA)
This is the criterion most products fail on.
- Normal text (under 18pt / 24px) has a contrast ratio of at least 4.5:1 against its background
- Large text (18pt+ bold or 24px+ regular) has a contrast ratio of at least 3:1
- This applies to both light and dark mode
- Placeholder text in form inputs meets contrast requirements
Tools: Use Chrome DevTools (Inspect > computed styles > contrast ratio), the axe browser extension, or Stark plugin for Figma.
1.4.4 Resize Text (Level AA)
- Text can be resized up to 200% without loss of content or functionality
- Use relative units (
rem,em,%) instead ofpxfor font sizes - Layouts don't break when browser zoom is set to 200%
1.4.5 Images of Text (Level AA)
- Text is rendered as actual text, not as images (exceptions: logos, decorative text in images)
- No screenshots of text used as content
1.4.10 Reflow (Level AA)
- Content reflows at 320px viewport width (400% zoom on a 1280px screen) without horizontal scrolling
- No two-dimensional scrolling except for content that requires it (data tables, maps, diagrams)
1.4.11 Non-text Contrast (Level AA)
- UI components (buttons, inputs, checkboxes, toggle switches) have at least 3:1 contrast against adjacent colors
- Focus indicators meet 3:1 contrast
- Chart lines, graph bars, and icons that convey meaning meet 3:1 contrast
1.4.12 Text Spacing (Level AA)
- Content and functionality are preserved when users override text spacing to:
- Line height: 1.5x the font size
- Letter spacing: 0.12x the font size
- Word spacing: 0.16x the font size
- Paragraph spacing: 2x the font size
- No content is clipped or overlapped when these overrides are applied
1.4.13 Content on Hover or Focus (Level AA)
- Tooltips and popovers that appear on hover/focus can be dismissed without moving the pointer (Escape key)
- Users can hover over the tooltip content itself without it disappearing
- The content stays visible until the user dismisses it, moves focus, or the trigger is no longer hovered
Principle 2: Operable
Users must be able to operate the interface. It can't require interactions that a user cannot perform.
2.1 Keyboard Accessible
2.1.1 Keyboard (Level A)
- Every interactive element is reachable and operable via keyboard (Tab, Enter, Space, Arrow keys, Escape)
- Custom components (dropdowns, modals, accordions, tabs) have appropriate keyboard handlers
- No functionality requires a mouse hover, drag, or multi-finger gesture as the only input method
2.1.2 No Keyboard Trap (Level A)
- Focus can always move away from any component using standard keyboard navigation
- Modals trap focus inside when open BUT release focus when closed
- No infinite focus loops
// Good: Modal with focus trap that releases on close
<Dialog open={isOpen} onClose={() => setIsOpen(false)}>
{/* Focus is trapped here while open */}
<DialogPanel>
<h2>Settings</h2>
<button onClick={() => setIsOpen(false)}>Close</button>
</DialogPanel>
</Dialog>
2.1.4 Character Key Shortcuts (Level A)
- If single-character key shortcuts exist (like "S" for search), they can be turned off, remapped, or only activate when the relevant component has focus
2.2 Enough Time
2.2.1 Timing Adjustable (Level A)
- Session timeouts give users a warning and the option to extend
- Auto-advancing carousels can be paused
- No content disappears on a timer without user control
2.2.2 Pause, Stop, Hide (Level A)
- Moving, blinking, or auto-updating content can be paused, stopped, or hidden
- Animations triggered by scrolling or interaction respect
prefers-reduced-motion
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
2.3 Seizures and Physical Reactions
2.3.1 Three Flashes or Below Threshold (Level A)
- No content flashes more than 3 times per second
2.4 Navigable
2.4.1 Bypass Blocks (Level A)
- A "skip to main content" link is available as the first focusable element
- Landmark regions (
<nav>,<main>,<aside>) allow screen reader users to jump between sections
// Skip link as first element in layout
<a href="#main-content" className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:px-4 focus:py-2 focus:bg-white focus:text-black focus:rounded">
Skip to main content
</a>
2.4.2 Page Titled (Level A)
- Every page has a unique, descriptive
<title>that describes its purpose - Title follows a consistent pattern (e.g., "Page Name | Site Name")
2.4.3 Focus Order (Level A)
- Tab order follows a logical sequence that matches the visual layout
- Dynamically added content (modals, toast notifications, expanded sections) receives focus appropriately
- No
tabindexvalues greater than 0
2.4.4 Link Purpose (In Context) (Level A)
- Link text describes where the link goes ("View pricing" not "Click here")
- If multiple links go to different places, their text is distinct
2.4.5 Multiple Ways (Level AA)
- Users can find pages through more than one method (navigation menu, search, sitemap, breadcrumbs)
2.4.6 Headings and Labels (Level AA)
- Headings describe the content that follows them
- Form labels describe what the input expects
- Labels are unique within a page (two different fields shouldn't have the same label)
2.4.7 Focus Visible (Level AA)
- All interactive elements have a visible focus indicator when navigated to via keyboard
- The focus indicator is clearly distinguishable (not just a faint outline)
- Custom focus styles maintain at least 3:1 contrast
/* Good: Clear, visible focus indicator */
:focus-visible {
outline: 2px solid #685FD4;
outline-offset: 2px;
}
2.5 Input Modalities
2.5.1 Pointer Gestures (Level A)
- Functionality that uses multi-point or path-based gestures (pinch, swipe, drag) can also be performed with a single pointer (click/tap)
2.5.2 Pointer Cancellation (Level A)
- Actions fire on the
upevent (mouseup/touchend), not ondown— allowing users to cancel by moving away before releasing
2.5.3 Label in Name (Level A)
- The visible text label of a component is included in its accessible name
- A button that says "Submit" has an accessible name that contains "Submit" (not "btn-action-01")
2.5.4 Motion Actuation (Level A)
- Functionality triggered by device motion (shake, tilt) can also be triggered by a standard UI control and can be disabled
Principle 3: Understandable
Users must be able to understand the information and the operation of the interface.
3.1 Readable
3.1.1 Language of Page (Level A)
- The
<html>element has alangattribute with the correct language code
<html lang="en">
3.1.2 Language of Parts (Level AA)
- Content in a different language than the page default is wrapped with the appropriate
langattribute
3.2 Predictable
3.2.1 On Focus (Level A)
- Focusing on an element does not automatically trigger a change of context (page navigation, form submission, modal opening)
3.2.2 On Input (Level A)
- Changing a form input value does not automatically trigger a change of context unless the user is warned beforehand
3.2.3 Consistent Navigation (Level AA)
- Navigation menus appear in the same location and order across pages
- The sidebar doesn't rearrange between pages
3.2.4 Consistent Identification (Level AA)
- Components with the same function have the same label across the app
- The "Save" button is always called "Save," not sometimes "Submit" and sometimes "Confirm"
3.3 Input Assistance
3.3.1 Error Identification (Level A)
- Form errors are identified in text, not just by color
- Error messages specify which field has the error and what's wrong
3.3.2 Labels or Instructions (Level A)
- Form fields have visible labels (not just placeholder text)
- Required fields are indicated (asterisk, "(required)" text, or
aria-required="true") - Format expectations are stated before input ("Date format: MM/DD/YYYY")
3.3.3 Error Suggestion (Level AA)
- When an input error is detected and suggestions are available, they're provided to the user
- "Invalid email" should suggest "Enter an email address like name@example.com"
3.3.4 Error Prevention (Legal, Financial, Data) (Level AA)
- Submissions that involve legal, financial, or data changes are reversible, verified, or confirmed
- Delete actions require confirmation
- Users can review input before final submission
// Good: Confirmation before destructive action
<Dialog>
<DialogTitle>Delete account?</DialogTitle>
<DialogDescription>
This will permanently delete your account and all associated data. This action cannot be undone.
</DialogDescription>
<button onClick={onCancel}>Cancel</button>
<button onClick={onConfirm}>Delete account</button>
</Dialog>
For more on accessible form patterns, see Form Validation That Doesn't Frustrate Users.
Principle 4: Robust
Content must be robust enough to be interpreted by a wide variety of user agents, including assistive technologies.
4.1 Compatible
4.1.1 Parsing (Level A)
- HTML is valid with no duplicate IDs on the same page
- All elements have complete start and end tags
- Elements are nested according to their specifications
4.1.2 Name, Role, Value (Level A)
- All interactive components have an accessible name, role, and state
- Custom components use appropriate ARIA roles (
role="dialog",role="tablist",role="menu") - State changes are communicated to assistive technology (
aria-expanded,aria-selected,aria-checked,aria-disabled)
// Good: Custom accordion with proper ARIA
<div role="region" aria-labelledby="section-heading">
<button
aria-expanded={isOpen}
aria-controls="section-content"
id="section-heading"
>
Account Settings
</button>
<div id="section-content" role="region" hidden={!isOpen}>
{/* Settings content */}
</div>
</div>
4.1.3 Status Messages (Level AA)
- Status messages (success notifications, error counts, search results updates) are announced to screen readers without receiving focus
- Use
role="status",role="alert", oraria-live="polite"for dynamic status updates
// Good: Toast notification announced to screen readers
<div role="status" aria-live="polite">
{savedSuccessfully && "Settings saved successfully."}
</div>
// Good: Alert for errors
<div role="alert">
{error && `Error: ${error.message}`}
</div>
Testing Checklist
Meeting WCAG AA requires testing beyond just reading the spec. Here's a minimum testing routine:
Automated Testing
- Run axe DevTools or Lighthouse accessibility audit on every page
- Add
eslint-plugin-jsx-a11yto your ESLint config for build-time checks - Include accessibility checks in your CI pipeline
Manual Keyboard Testing
- Navigate the entire app using only Tab, Shift+Tab, Enter, Space, Arrow keys, and Escape
- Verify you can complete every user flow without a mouse
- Check that focus indicators are always visible during keyboard navigation
Screen Reader Testing
- Test with VoiceOver (macOS) or NVDA (Windows) on at least the main user flows
- Verify that headings, landmarks, and form labels are announced correctly
- Confirm that dynamic content changes (loading states, error messages, new items) are announced
Visual Testing
- Test at 200% browser zoom
- Test at 320px viewport width
- Test with
prefers-reduced-motion: reduceenabled - Test in both light and dark mode
- Use a contrast checker on all text and interactive elements
Pre-Built Accessible Components
Building every accessible pattern from scratch is time-consuming and error-prone. Component libraries that handle accessibility out of the box can save weeks of work:
- shadcn/ui + Radix UI: Unstyled, accessible primitives that you style with Tailwind CSS. Used by the thefrontkit SaaS Starter Kit.
- Headless UI: Accessible components from the Tailwind CSS team.
- React Aria (Adobe): Low-level accessibility hooks for building custom components.
The thefrontkit SaaS Starter Kit and AI UX Kit ship with WCAG AA compliance built into every component: keyboard navigation, ARIA attributes, focus management, and color contrast all pass out of the box. This means you start with an accessible foundation and only need to maintain compliance as you customize and extend.
For more on building accessible products, see:
- Why Accessibility Should Be Your First Priority
- Customize UI Kits Without Breaking Accessibility
- Accessibility for Startup Founders
- Win RFPs with Accessibility Receipts
- Building an Accessible React Dashboard
Start with accessible components: View SaaS Starter Kit | View AI UX Kit | Browse all templates