Why Accessibility Matters
Roughly 1.3 billion people worldwide — about 16% of the global population — have a significant disability, according to the World Health Organization. That includes people with visual impairments (who use screen readers or screen magnifiers), motor impairments (who navigate with keyboards, switches, or voice control), hearing impairments (who need captions and transcripts), and cognitive impairments (who benefit from clear language and consistent navigation).
Beyond ethics, accessibility is increasingly a legal requirement. The European Accessibility Act (EAA) takes effect in June 2025, requiring digital products and services in the EU to meet accessibility standards. In the United States, ADA lawsuits targeting inaccessible websites have increased every year since 2018. Section 508 applies to all US federal agencies and their vendors.
There is also a practical argument: accessible code is better code. Semantic HTML improves SEO. Keyboard navigation benefits power users. Captions help everyone in noisy environments. High contrast is easier to read in sunlight. When you build for accessibility, you improve the experience for all users.
The Click-Away Pound survey found that 69% of users with disabilities click away from websites with accessibility barriers. That is lost revenue. Accessible websites have wider reach, better SEO, and lower legal risk. It is not charity — it is good engineering and good business.
WCAG Levels: A, AA, AAA
The Web Content Accessibility Guidelines (WCAG) are published by the W3C and define measurable success criteria organized into three conformance levels.
| Level | What It Covers | Who Targets It |
|---|---|---|
| A | Basic requirements: alt text, keyboard access, no seizure-inducing content, no keyboard traps | Absolute minimum; failing Level A means severe barriers |
| AA | Adds: color contrast (4.5:1), text resize to 200%, consistent navigation, error identification in forms, visible focus indicators | Legal standard in most jurisdictions; what most organizations should target |
| AAA | Adds: enhanced contrast (7:1), sign language for audio, no timing limits, simplified language | Specialized use cases; not practical as a blanket requirement |
WCAG 2.2 (the current version as of 2026) organizes its criteria under four principles, often abbreviated as POUR:
- Perceivable — users must be able to perceive the content (text alternatives, captions, contrast)
- Operable — users must be able to operate the interface (keyboard access, enough time, no seizure triggers)
- Understandable — content must be understandable (readable text, predictable behavior, error prevention)
- Robust — content must work with current and future assistive technologies (valid markup, correct ARIA usage)
Semantic HTML: The Foundation
The single most impactful thing you can do for accessibility is use the right HTML elements. Screen readers, keyboard navigation, and browser features all depend on the semantic meaning of elements, not their visual appearance.
<!-- BAD: no semantic meaning, no keyboard access, no screen reader support -->
<div class="header">
<div class="nav">
<div class="link" onclick="navigate('/about')">About</div>
<div class="link" onclick="navigate('/contact')">Contact</div>
</div>
</div>
<div class="main">
<div class="title">Page Title</div>
<div class="text">Content here</div>
</div>
<!-- GOOD: screen readers announce landmarks, keyboard nav works, SEO benefits -->
<header>
<nav aria-label="Main navigation">
<a href="/about">About</a>
<a href="/contact">Contact</a>
</nav>
</header>
<main>
<h1>Page Title</h1>
<p>Content here</p>
</main>
Key Semantic Elements
| Element | Purpose | Screen Reader Behavior |
|---|---|---|
<header> |
Page or section header | Announced as "banner" landmark |
<nav> |
Navigation links | Announced as "navigation" landmark |
<main> |
Primary content | Announced as "main" landmark; users can jump to it |
<article> |
Self-contained content | Announced as "article" |
<aside> |
Tangentially related content | Announced as "complementary" landmark |
<footer> |
Page or section footer | Announced as "contentinfo" landmark |
<button> |
Clickable action | Announced as "button"; focusable; Enter/Space activates |
<a href> |
Link to another page | Announced as "link"; focusable; Enter activates |
When converting legacy HTML into properly structured semantic markup, the HTML Beautifier can help you reformat and clean up the document structure as you refactor.
ARIA: When Native HTML Falls Short
ARIA (Accessible Rich Internet Applications) is a set of attributes that provide additional semantics to assistive technologies. The first rule of ARIA, stated in the official specification, is: do not use ARIA if you can use native HTML.
<!-- BAD: ARIA reimplementing what a button already does -->
<div role="button" tabindex="0" aria-pressed="false"
onkeydown="if(event.key==='Enter'||event.key===' ')this.click()"
onclick="toggleMenu()">
Menu
</div>
<!-- GOOD: native button does all of this for free -->
<button onclick="toggleMenu()">Menu</button>
When ARIA Is Needed
- Custom widgets — tabs, accordions, comboboxes, and tree views have no single native HTML element. Use ARIA roles (
role="tablist",role="tab",role="tabpanel") and properties (aria-selected,aria-controls). - Live regions — when content updates dynamically (notifications, chat messages, form validation), use
aria-live="polite"oraria-live="assertive"to announce changes to screen readers. - Labels and descriptions —
aria-label,aria-labelledby, andaria-describedbyprovide context that the visual layout implies but the DOM structure does not. - State communication —
aria-expanded,aria-hidden,aria-disabled, andaria-currenttell screen readers about the current state of interactive elements.
<div role="tablist" aria-label="Account settings">
<button role="tab" id="tab-1" aria-selected="true" aria-controls="panel-1">
Profile
</button>
<button role="tab" id="tab-2" aria-selected="false" aria-controls="panel-2"
tabindex="-1">
Security
</button>
</div>
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
<p>Profile settings content</p>
</div>
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
<p>Security settings content</p>
</div>
Adding role="heading" to a <div> tells screen readers it is a heading, but it does not appear in the document outline, is not picked up by heading navigation shortcuts, and will not be styled correctly by default. Use <h2> instead. ARIA changes what assistive technology announces, but it does not change browser behavior.
Keyboard Navigation
Every interactive element on your page must be operable with a keyboard. This is a Level A requirement, and it is the accessibility criterion that fails most often.
Default Keyboard Behavior
| Key | Action |
|---|---|
Tab |
Move focus to next focusable element |
Shift + Tab |
Move focus to previous focusable element |
Enter |
Activate links and buttons |
Space |
Activate buttons, toggle checkboxes, scroll page |
Escape |
Close modals, menus, dropdowns |
Arrow keys |
Navigate within composite widgets (tabs, menus, radio groups) |
Focus Visibility
Users must be able to see which element has focus. Never remove the focus outline without providing an alternative.
/* BAD: removes focus indicator entirely */
*:focus { outline: none; }
/* GOOD: custom focus style that is visible on all backgrounds */
:focus-visible {
outline: 2px solid #00d4ff;
outline-offset: 2px;
border-radius: 4px;
}
/* Hide focus for mouse clicks, show for keyboard */
:focus:not(:focus-visible) {
outline: none;
}
The :focus-visible pseudo-class shows the focus ring only when the user is navigating with a keyboard (Tab key), not when clicking with a mouse. This gives you keyboard accessibility without the visual distraction of focus rings on every mouse click.
Skip Links
A skip link lets keyboard users jump past the navigation to the main content. It is hidden visually but appears when focused.
<!-- First element in the body -->
<a href="#main-content" class="skip-link">Skip to main content</a>
<!-- ... navigation ... -->
<main id="main-content">
<!-- page content -->
</main>
<style>
.skip-link {
position: absolute;
top: -100%;
left: 16px;
padding: 8px 16px;
background: #00d4ff;
color: #fff;
border-radius: 0 0 8px 8px;
z-index: 9999;
text-decoration: none;
font-weight: 600;
}
.skip-link:focus {
top: 0;
}
</style>
Color Contrast
Insufficient color contrast is the most common accessibility defect found by automated scanners. WCAG 2.2 defines minimum contrast ratios between text and its background.
| Text Type | Level AA | Level AAA |
|---|---|---|
| Normal text (under 18pt / 14pt bold) | 4.5:1 | 7:1 |
| Large text (18pt+ / 14pt+ bold) | 3:1 | 4.5:1 |
| Non-text (icons, borders, focus indicators) | 3:1 | Not defined |
Check your color combinations with the Color Contrast Checker — it calculates the contrast ratio, shows WCAG pass/fail for all levels, and suggests adjustments if your colors fail. For a broader view of how your palette looks to users with color vision deficiency, the Color Blindness Simulator renders your design through protanopia, deuteranopia, and tritanopia filters.
Placeholder text in form inputs (gray on white), disabled button text (gray on gray), link text that relies on color alone (no underline), and light text on gradient or image backgrounds are the most frequent contrast violations. Always check contrast against the actual background, not a solid approximation.
Images and Media
Images
Every <img> element must have an alt attribute. The content of that attribute depends on the image's purpose.
<!-- Informative image: describe what it shows -->
<img src="chart.png" alt="Bar chart showing revenue growth from $2M in Q1 to $5M in Q4">
<!-- Decorative image: empty alt (not missing, empty) -->
<img src="decorative-swirl.svg" alt="">
<!-- Functional image (inside a link): describe the action -->
<a href="/home">
<img src="logo.svg" alt="QTool home page">
</a>
<!-- Complex image: brief alt + longer description -->
<figure>
<img src="architecture-diagram.png"
alt="System architecture diagram"
aria-describedby="arch-desc">
<figcaption id="arch-desc">
The system consists of three layers: the API gateway handles routing,
the service mesh manages inter-service communication, and the data
layer includes PostgreSQL for transactional data and Redis for caching.
</figcaption>
</figure>
Video and Audio
- Captions — all pre-recorded video with speech must have synchronized captions (Level A)
- Transcripts — pre-recorded audio-only content must have a text transcript (Level A)
- Audio descriptions — video where important visual information is not conveyed in the audio track needs an audio description (Level AA)
- No autoplay — do not auto-play audio or video with sound. If you must, provide a visible mechanism to pause or stop it within the first 3 seconds.
Accessible Forms
Forms are where accessibility failures cause the most real-world harm. If a user cannot fill out a registration form, checkout process, or support request, you have locked them out of your service.
<form>
<!-- Every input needs a label -->
<div>
<label for="email">Email address</label>
<input type="email" id="email" name="email"
required
aria-describedby="email-help email-error"
autocomplete="email">
<p id="email-help" class="help-text">We will never share your email.</p>
<p id="email-error" class="error" role="alert" hidden>
Please enter a valid email address.
</p>
</div>
<!-- Group related fields -->
<fieldset>
<legend>Notification preferences</legend>
<label>
<input type="checkbox" name="notify_email"> Email notifications
</label>
<label>
<input type="checkbox" name="notify_sms"> SMS notifications
</label>
</fieldset>
<button type="submit">Save preferences</button>
</form>
Form Accessibility Rules
- Every input needs a visible label. Placeholder text is not a label — it disappears when the user types.
- Use
for/idpairing so clicking the label focuses the input. Screen readers announce the label when the input receives focus. - Group related fields with
<fieldset>and<legend>. Radio buttons and checkbox groups must be in a fieldset. - Error messages must identify the field and describe how to fix the error. Use
role="alert"oraria-live="assertive"so screen readers announce errors immediately. - Use
autocompleteattributes on fields for personal data (name, email, address, phone). This helps password managers and assistive technology fill forms correctly.
Test the structure of your form markup with the Accessibility Checker tool — it scans your HTML for missing labels, incorrect ARIA usage, and contrast issues.
Dynamic Content and SPAs
Single-page applications (SPAs) built with React, Vue, or Angular present unique accessibility challenges because the page does not reload on navigation. Screen readers are not notified of route changes, focus is not managed automatically, and dynamically injected content is invisible to assistive technology unless you announce it.
Route Change Announcements
function RouteAnnouncer() {
const location = useLocation();
const [announcement, setAnnouncement] = useState('');
useEffect(() => {
// Announce the new page title to screen readers
setAnnouncement(`Navigated to ${document.title}`);
}, [location]);
return (
<div
role="status"
aria-live="polite"
aria-atomic="true"
className="sr-only" /* visually hidden */
>
{announcement}
</div>
);
}
Live Regions for Dynamic Updates
<!-- Polite: announced after current speech finishes -->
<div aria-live="polite">
3 items in your cart
</div>
<!-- Assertive: interrupts current speech immediately -->
<div aria-live="assertive" role="alert">
Your session will expire in 2 minutes.
</div>
Focus Management
After a route change, move focus to the new page's <h1> or <main> element. After an action (adding to cart, submitting a form, closing a modal), move focus to a logical location — typically the element that triggered the action or a confirmation message.
// After closing a modal, return focus to the trigger
function closeModal(triggerElement) {
modal.hidden = true;
triggerElement.focus();
}
// After a route change, focus the main heading
function onRouteChange() {
const heading = document.querySelector('h1');
if (heading) {
heading.setAttribute('tabindex', '-1');
heading.focus();
}
}
Testing for Accessibility
No single tool catches every accessibility issue. You need a combination of automated scanning, manual keyboard testing, and screen reader testing.
Automated Testing
Automated tools catch about 30-40% of WCAG issues. They are fast and consistent, making them ideal for CI/CD pipelines.
// Using axe-core with Playwright
const { test, expect } = require('@playwright/test');
const AxeBuilder = require('@axe-core/playwright').default;
test('home page has no accessibility violations', async ({ page }) => {
await page.goto('https://example.com');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag22aa'])
.analyze();
expect(results.violations).toEqual([]);
});
Manual Keyboard Testing Checklist
- Put your mouse aside. Navigate the entire page with
Tab,Shift+Tab,Enter,Space,Escape, and arrow keys. - Can you see where the focus is at all times? (Focus indicator visible?)
- Can you reach every interactive element? (No elements only reachable by mouse?)
- Can you activate every button and link? (Enter for links, Enter/Space for buttons?)
- Can you open and close every dropdown, modal, and menu? (Escape to close?)
- Is focus trapped inside modals? (Tab does not escape behind the overlay?)
- After closing a dialog, does focus return to the trigger element?
- Is there a skip link to jump past navigation?
Screen Reader Testing
| Platform | Screen Reader | Browser |
|---|---|---|
| macOS | VoiceOver (built in) | Safari (best support) |
| Windows | NVDA (free) or JAWS | Chrome or Firefox |
| Android | TalkBack (built in) | Chrome |
| iOS | VoiceOver (built in) | Safari |
Start with VoiceOver on macOS (Cmd+F5 to toggle). Navigate a page with the rotor (VO+U) and listen for: are headings in the right order? Are images described? Are buttons labeled? Are form fields announced with their labels? Is dynamic content announced?
For checking how your design system's colors work together across contrast requirements, the Color Palette Generator can help you build palettes that meet WCAG AA contrast requirements from the start.
Check Accessibility in Your Browser
QTool's free accessibility tools let you test color contrast, simulate color blindness, and audit HTML markup — all without installing anything.
Open Contrast Checker Accessibility CheckerAccessibility Tools
Frequently Asked Questions
Web accessibility (often abbreviated as a11y) means designing and building websites that can be used by everyone, including people with visual, auditory, motor, or cognitive disabilities. This covers people who use screen readers, keyboard-only navigation, screen magnifiers, voice control, and other assistive technologies. It matters for three reasons: it is the right thing to do (roughly 16% of the global population has a disability), it is increasingly a legal requirement (the ADA, EAA, and Section 508 apply to websites in many jurisdictions), and it improves the experience for all users (captions help in noisy environments, keyboard navigation helps power users, clear structure helps everyone). Accessible sites also tend to have better SEO because search engines rely on the same semantic structure that screen readers use.
WCAG (Web Content Accessibility Guidelines) defines three conformance levels. Level A is the minimum: it covers the most basic accessibility requirements like providing text alternatives for images, ensuring content can be accessed without color alone, and making all functionality available from a keyboard. Level AA is the standard that most laws and regulations require: it adds requirements like sufficient color contrast (4.5:1 for normal text, 3:1 for large text), text resizing up to 200% without loss of content, and consistent navigation across pages. Level AAA is the highest level: it includes enhanced contrast (7:1), sign language for audio, and more. Most organizations target Level AA because it covers the majority of accessibility needs without the extreme requirements of AAA that can conflict with some design needs.
Always prefer semantic HTML over ARIA. A native button element already has the correct role, keyboard behavior, and focus management built in. Adding role='button' to a div recreates what button provides for free, and usually incompletely (you still need to handle keyboard events, focus styles, and the disabled state yourself). The first rule of ARIA is literally 'do not use ARIA if you can use native HTML.' Use ARIA only when there is no native HTML element for what you are building: custom components like tabs, accordions, comboboxes, and modals that have no single HTML equivalent. Also use ARIA for live regions (aria-live) to announce dynamic content changes to screen readers, and for relationships between elements (aria-describedby, aria-labelledby) that are not implied by the DOM structure.
Use a combination of automated tools and manual testing. Automated tools like axe DevTools, Lighthouse, and WAVE catch about 30-40% of accessibility issues (missing alt text, contrast violations, missing labels, incorrect ARIA usage). For the other 60-70%, you need manual testing: navigate the entire page using only a keyboard (Tab, Enter, Escape, arrow keys) to verify that all interactive elements are reachable and operable; test with a screen reader (VoiceOver on Mac, NVDA on Windows, TalkBack on Android) to verify that content is announced correctly and in the right order; zoom to 200% and check that no content is cut off or overlapping; and check that all information conveyed by color is also available without color. Integrate automated tests into your CI/CD pipeline using axe-core or pa11y, and schedule manual screen reader testing at least once per release.
WCAG 2.2 Level AA requires a contrast ratio of at least 4.5:1 for normal text (under 18pt or 14pt bold) and 3:1 for large text (18pt and above, or 14pt bold and above). Level AAA requires 7:1 for normal text and 4.5:1 for large text. Non-text elements like form field borders, icons that convey meaning, and focus indicators require at least 3:1 contrast against their background. These ratios are calculated using relative luminance values of the foreground and background colors. You can check contrast ratios using browser DevTools (Chrome highlights failing elements), online tools like the QTool Color Contrast Checker, or design tools like Figma's built-in contrast checker. Always test against the actual background the text appears on, including gradients and images.
An accessible modal requires four things. First, focus management: when the modal opens, move focus to the first focusable element inside it (or the modal container itself if it has a heading). When it closes, return focus to the element that triggered it. Second, focus trapping: Tab and Shift+Tab should cycle through focusable elements inside the modal and not escape to the page behind it. Third, keyboard dismissal: pressing Escape should close the modal. Fourth, ARIA markup: use role='dialog' and aria-modal='true' on the container, and aria-labelledby pointing to the modal's heading. The native HTML dialog element (supported in all modern browsers) provides most of this behavior for free: it handles focus trapping, the Escape key, and the correct ARIA role automatically. Use dialog.showModal() to open it as a modal.