I shipped a client project last year that looked beautiful. Smooth animations, perfect color palette, custom fonts. Then the client’s accessibility audit came back: 47 violations. I’d built a site that 15% of the world’s population couldn’t use properly.
Accessibility isn’t a nice-to-have — it’s a legal requirement in many jurisdictions and, honestly, it’s just good engineering. Here’s the checklist I now run through on every project.
Understanding WCAG 2.1: The Standards Behind Accessibility
The Web Content Accessibility Guidelines (WCAG) are the international standard for web accessibility, maintained by the W3C’s Web Accessibility Initiative (WAI). WCAG 2.1 organizes its success criteria into three conformance levels, each building upon the previous one.
| Level | What It Covers | Who Needs It | Example Criteria |
|---|---|---|---|
| A | Basic accessibility (alt text, keyboard access, no seizure-inducing content) | Everyone — bare minimum | Text alternatives, keyboard access, no auto-playing audio |
| AA | Enhanced accessibility (contrast ratios, text resizing, consistent navigation) | Most legal requirements (ADA, EAA, Section 508) | 4.5:1 contrast ratio, text resizeable to 200%, visible focus indicators |
| AAA | Maximum accessibility (sign language, extended audio descriptions, enhanced contrast) | Specialized needs, government portals | 7:1 contrast ratio, no timing limits, pronunciation support |
Target Level AA. It’s what the Americans with Disabilities Act (ADA), the European Accessibility Act (EAA), and Section 508 reference. Achieving AAA across an entire site is often impractical for most applications, but individual AAA criteria can be valuable to implement selectively.
WCAG’s Four Principles: POUR
Every WCAG criterion falls under one of four principles, commonly remembered by the acronym POUR:
| Principle | Meaning | Key Question |
|---|---|---|
| Perceivable | Users must be able to perceive the content | Can users see, hear, or touch all information? |
| Operable | Users must be able to operate the interface | Can users navigate and interact with all controls? |
| Understandable | Users must understand the information and interface | Is the content readable and predictable? |
| Robust | Content must work with current and future technologies | Does it work with assistive technologies? |
Semantic HTML: The Foundation of Accessible Web Pages
Using the right HTML elements is the single highest-impact accessibility improvement you can make. Screen readers and other assistive technologies rely entirely on semantic meaning to convey page structure to users.
Use Native Elements Instead of Generic DIVs
The most common accessibility mistake is building custom interactive elements from <div> and <span> tags. Native HTML elements carry built-in semantics, keyboard behavior, and ARIA roles that custom elements do not.
<!-- Bad: screen readers don't know this is navigation -->
<div class="nav">
<div class="nav-item" onclick="navigate('/')">Home</div>
</div>
<!-- Good: screen readers announce "navigation" -->
<nav aria-label="Main navigation">
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
When you use <nav>, screen readers automatically announce “navigation landmark.” When you use <a>, the element is automatically focusable, activatable with Enter, and announced as a link with its destination. A <div> with onclick provides none of these behaviors — you would need to manually add role="link", tabindex="0", keyboard event listeners, and focus styling to achieve what <a> gives you for free.
Heading Hierarchy: The Document Outline
Headings create the document outline that screen reader users navigate by. Many screen reader users scan a page by jumping between headings (pressing H in NVDA or JAWS) before reading content, much like sighted users scan visual headings.
<!-- Bad: heading levels skipped, misleading structure -->
<h1>Page Title</h1>
<h4>Looks right visually but breaks the outline</h4>
<!-- Good: proper hierarchy, use CSS for visual sizing -->
<h1>Page Title</h1>
<h2>Section Title</h2>
<h3>Subsection</h3>
Rules for headings:
- One
<h1>per page — it should describe the page’s primary purpose - Never skip heading levels (e.g., don’t jump from
<h2>to<h4>) - Use CSS classes for visual sizing — don’t choose a heading level based on its default font size
- Headings should be descriptive, not generic (use “Shipping Options” not “Options”)
HTML5 Landmark Elements
Landmark elements define the major regions of a page. Screen readers allow users to jump directly between landmarks, providing efficient navigation for long pages.
| Element | Implicit ARIA Role | Purpose | Usage Notes |
|---|---|---|---|
<header> | banner | Site header with logo and global navigation | Only the top-level <header> gets banner role |
<nav> | navigation | Navigation links | Use aria-label to distinguish multiple navs |
<main> | main | Primary content area | Exactly one per page |
<aside> | complementary | Sidebar or tangentially related content | Should make sense independently |
<footer> | contentinfo | Site footer with legal links and contact info | Only the top-level <footer> gets this role |
<article> | article | Self-contained, independently distributable content | Blog posts, news articles, forum posts |
<section> | region | Thematic grouping of content | Only gets region role if it has an accessible name |
Keyboard Navigation: Making Every Control Accessible
Every interactive element on your page must be fully operable with a keyboard alone. Approximately 2-3% of web users rely exclusively on keyboard navigation due to motor disabilities, temporary injuries, or personal preference.
Focus Indicators: Making the Current Element Visible
When a user tabs through your page, they need a clear visual indicator of which element is currently focused. Removing focus outlines is one of the most harmful accessibility mistakes developers make.
/* Never do this globally — it removes all focus indicators */
*:focus { outline: none; }
/* Instead, use :focus-visible for smart focus management */
:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
border-radius: 4px;
}
/* Optionally hide focus ring for mouse users (already handled by :focus-visible) */
:focus:not(:focus-visible) {
outline: none;
}
The :focus-visible pseudo-class is the modern best practice — it shows the focus ring for keyboard navigation but hides it for mouse clicks, giving both user groups an optimal experience.
Tab Order and Interactive Element Rules
| Element | Focusable by Default | Keyboard Activation | Notes |
|---|---|---|---|
<a href> | ✅ Yes | Enter | Must have href to be focusable |
<button> | ✅ Yes | Enter or Space | Preferred for actions that don’t navigate |
<input /> | ✅ Yes | Varies by type | Use appropriate type attributes |
<select> | ✅ Yes | Space to open, arrows to navigate | Arrow keys move between options |
<textarea> | ✅ Yes | N/A (text input) | Tab moves focus away, not inserting a tab character |
<div> | ❌ No | N/A | Never use for interactive elements |
<span> | ❌ No | N/A | Never use for interactive elements |
<!-- Bad: not keyboard-accessible without significant extra work -->
<div class="button" onclick="submit()">Submit</div>
<!-- Good: handles Enter, Space, focus, and screen reader announcement automatically -->
<button type="submit">Submit</button>
Critical rule: Never use positive tabindex values (e.g., tabindex="5"). They override the natural DOM order and create unpredictable, confusing tab sequences. Only use tabindex="0" (to make an element focusable in DOM order) or tabindex="-1" (to make it programmatically focusable but not in the tab order).
Skip Navigation Links
For pages with extensive navigation menus, provide a “skip to main content” link as the first focusable element. This allows keyboard users to bypass repetitive navigation on every page.
<body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<nav><!-- extensive navigation --></nav>
<main id="main-content"><!-- page content --></main>
</body>
.skip-link {
position: absolute;
top: -100%;
left: 0;
padding: 0.5rem 1rem;
background: var(--color-accent);
color: white;
z-index: 100;
}
.skip-link:focus {
top: 0;
}
Color and Contrast: Ensuring Visual Accessibility
Color-related accessibility issues affect a large portion of users. Approximately 8% of men and 0.5% of women have some form of color vision deficiency. Beyond color blindness, many users work in challenging lighting conditions — bright sunlight, dim rooms, or low-quality displays — where contrast becomes critical.
WCAG Contrast Requirements
| Element Type | Minimum Ratio (Level AA) | Enhanced Ratio (Level AAA) | How to Measure |
|---|---|---|---|
| Normal text (< 18pt) | 4.5:1 | 7:1 | Foreground vs. background color |
| Large text (≥ 18pt or ≥ 14pt bold) | 3:1 | 4.5:1 | Foreground vs. background color |
| UI components and graphical objects | 3:1 | Not specified | Component vs. adjacent color |
| Non-text contrast (icons, borders) | 3:1 | Not specified | Element vs. background |
Check your colors: Use our Color Converter to get exact RGB values and calculate contrast ratios. Our Color Palette Generator creates harmonious schemes — just verify each combination meets the WCAG ratios listed above.
Don’t Rely on Color Alone
Information conveyed through color must also be available through other visual means — text labels, icons, patterns, or shapes. This is WCAG criterion 1.4.1 and one of the most commonly violated.
<!-- Bad: only color indicates the error state -->
<input style="border-color: red;" />
<!-- Good: color + icon + descriptive text -->
<input aria-invalid="true" aria-describedby="email-error" />
<span id="email-error" role="alert">
⚠️ Please enter a valid email address
</span>
Common violations to watch for:
- Form validation that only changes border color on error
- Charts and graphs that distinguish data series only by color
- Links that are the same color as surrounding text with no underline
- Status indicators (online/offline) using only green/red dots without labels
Images, Media, and Alternative Content
Alt Text: The Rules for Every Image Type
Every <img /> element must have an alt attribute, but the content of that attribute depends on the image’s purpose.
| Image Type | Alt Text Strategy | Example |
|---|---|---|
| Informative | Describe the content or message | alt="Sales revenue increased 40% in Q3 2025" |
| Functional | Describe the action or destination | alt="Search", alt="Download PDF report" |
| Decorative | Empty alt attribute | alt="" (not omitted — explicitly empty) |
| Complex (charts, diagrams) | Brief alt + longer description | alt="Q3 revenue chart" with aria-describedby linking to a detailed text description |
| Text in image | Reproduce the text exactly | alt="50% OFF — Summer Sale" |
<!-- Informative image -->
<img src="chart.png" alt="Sales increased 40% in Q3, reaching $2.4M" />
<!-- Functional image (inside a link) -->
<a href="/search"><img src="search-icon.png" alt="Search" /></a>
<!-- Decorative image (adds no information) -->
<img src="decorative-border.png" alt="" />
<!-- Complex image with extended description -->
<figure>
<img src="architecture.png" alt="System architecture diagram" aria-describedby="arch-desc" />
<figcaption id="arch-desc">
The system consists of three layers: a React frontend communicating via REST API
with a Node.js backend, which connects to a PostgreSQL database...
</figcaption>
</figure>
Video and Audio Accessibility
Video content requires captions for deaf and hard-of-hearing users, and audio descriptions for blind users. Audio-only content requires text transcripts.
<video controls>
<source src="demo.mp4" type="video/mp4" />
<track kind="captions" src="captions-en.vtt" srclang="en" label="English" default />
<track kind="descriptions" src="descriptions-en.vtt" srclang="en" label="Audio Descriptions" />
</video>
ARIA: Bridging the Gap When HTML Falls Short
The first rule of ARIA (Accessible Rich Internet Applications) is: don’t use ARIA if a native HTML element already provides the semantics you need. ARIA adds semantic information for assistive technologies, but it does not add behavior — you still need to implement keyboard interaction, focus management, and visual states yourself.
Essential ARIA Patterns for Web Applications
| Pattern | ARIA Attributes | When to Use |
|---|---|---|
| Live regions | aria-live="polite" or aria-live="assertive" | Dynamic content updates (notifications, chat messages, form validation) |
| Expanded/collapsed | aria-expanded="true/false", aria-controls="id" | Accordions, dropdown menus, collapsible panels |
| Current page | aria-current="page" | Highlighting the active page in navigation |
| Described by | aria-describedby="id" | Connecting form inputs to help text or error messages |
| Required fields | aria-required="true" | Form fields that must be filled (prefer HTML required attribute) |
| Disabled state | aria-disabled="true" | Elements that are visible but not interactive |
| Modal dialogs | role="dialog", aria-modal="true", aria-labelledby="id" | Modal windows that trap focus |
<!-- Live region: polite announcement (waits for user to finish reading) -->
<div aria-live="polite" aria-atomic="true">
3 items added to cart — Total: $45.99
</div>
<!-- Icon button with accessible label -->
<button aria-label="Close dialog" type="button">
<svg aria-hidden="true"><!-- X icon --></svg>
</button>
<!-- Accordion with expanded/collapsed state -->
<button aria-expanded="false" aria-controls="faq-answer-1">
What is your return policy?
</button>
<div id="faq-answer-1" hidden>
We offer a 30-day return policy for all unused items...
</div>
<!-- Navigation with current page indicator -->
<nav aria-label="Main">
<a href="/" aria-current="page">Home</a>
<a href="/about">About</a>
<a href="/contact">Contact</a>
</nav>
Common ARIA Mistakes
| Mistake | Why It’s Wrong | Fix |
|---|---|---|
role="button" on a <div> | Missing keyboard support and focus | Use <button> instead |
aria-label on a non-interactive <div> | Screen readers may not announce it | Use visible text or <p> |
aria-hidden="true" on focusable elements | Creates a focus trap for screen reader users | Remove from tab order first |
Duplicate aria-label and visible text | Screen readers announce both, causing confusion | Use one or the other |
Forms: Accessible Input Patterns
Forms are one of the most interaction-heavy parts of any web application, and they are where accessibility issues cause the most user frustration.
Label Association
Every form input must have a programmatically associated label. There are two valid methods:
<!-- Method 1: Wrapping (implicit association) -->
<label>
Email address
<input type="email" name="email" required />
</label>
<!-- Method 2: for/id (explicit association — preferred) -->
<label for="user-email">Email address</label>
<input type="email" id="user-email" name="email" required />
Error Handling and Validation Feedback
When form validation fails, users need to know what went wrong, which field has the error, and how to fix it. This information must be available to both sighted users and screen reader users.
<label for="password">Password</label>
<input
type="password"
id="password"
aria-invalid="true"
aria-describedby="password-error password-requirements"
required
/>
<p id="password-error" role="alert" class="error">
Password must be at least 8 characters long
</p>
<p id="password-requirements" class="help-text">
Include at least one uppercase letter, one number, and one special character
</p>
Testing: A Multi-Layered Approach
No single tool catches all accessibility issues. Automated tools catch approximately 30-40% of WCAG violations. The remaining 60-70% require manual testing and user testing.
Testing Strategy by Method
| Method | Coverage | Tools | What It Catches |
|---|---|---|---|
| Automated scanning | ~30-40% | axe DevTools, Lighthouse, WAVE, eslint-plugin-jsx-a11y | Missing alt text, contrast failures, missing labels, duplicate IDs |
| Keyboard testing | ~20% | Unplug your mouse, use Tab/Shift+Tab/Enter/Space/Escape | Focus traps, unreachable elements, invisible focus indicators, incorrect tab order |
| Screen reader testing | ~20% | VoiceOver (macOS), NVDA (Windows), TalkBack (Android) | Missing announcements, confusing reading order, unlabeled controls |
| Visual inspection | ~10% | Browser zoom to 200%, Windows High Contrast Mode, forced colors | Layout breakage at zoom, information lost in high contrast, text truncation |
| User testing | Remaining gaps | Real users with disabilities | Cognitive issues, unexpected workflows, real-world usability problems |
Essential Manual Test Procedures
Keyboard-only navigation test:
- Unplug your mouse or disable your trackpad
- Press Tab to move through all interactive elements — can you reach everything?
- Press Enter or Space on buttons and links — do they activate?
- Press Escape to close modals and menus — does focus return to the trigger?
- Check that focus order is logical (left to right, top to bottom)
- Verify focus indicators are visible on every focused element
200% zoom test:
- Zoom your browser to 200% (Ctrl + +)
- No content should require horizontal scrolling
- No text should be cut off or overlap other elements
- All interactive elements should remain usable
The Complete Accessibility Checklist
Use this checklist on every project before launch:
Structure and Semantics
- One
<h1>per page with proper heading hierarchy (no skipped levels) - Landmark elements used (
<main>,<nav>,<header>,<footer>) - Page language declared (
<html lang="en">) - Page title is descriptive and unique
Keyboard and Interaction
- All interactive elements keyboard-accessible (Tab, Enter, Space, Escape)
- Focus indicators visible on every focusable element (
:focus-visible) - No keyboard traps (user can always Tab away from any element)
- Skip navigation link provided for long pages
- Touch targets at least 44×44px on mobile
Visual Design
- Text color contrast meets 4.5:1 for normal text, 3:1 for large text
- UI component contrast meets 3:1 against adjacent colors
- Information not conveyed by color alone (use text, icons, or patterns)
- Content readable and functional at 200% browser zoom
-
prefers-reduced-motionmedia query respected for animations
Images and Media
- All images have appropriate
altattributes (informative, functional, or empty) - Complex images have extended descriptions
- Videos have captions (closed or open)
- Audio content has text transcripts
Forms
- All form inputs have programmatically associated labels
- Required fields are indicated (not just by color)
- Error messages are descriptive, visible, and announced by screen readers (
role="alert") - Form instructions appear before the form, not only after submission
Dynamic Content
- Live regions announce dynamic content changes (
aria-live) - Modal dialogs trap focus and return focus on close
- Expanded/collapsed states communicated (
aria-expanded) - Loading states communicated to screen readers
Further Reading
- WCAG Color Contrast and Accessibility
- SEO Meta Tags: The Complete Checklist
- Dark Mode Color Palette Guide
Building accessible interfaces? Use our Color Converter to verify contrast ratios, HTML Formatter for clean semantic markup, and Meta Tag Generator for accessible page metadata.