Accessibility is not a polish pass before launch. It is how people with disabilities, slow connections, broken mice, bright sunlight, or voice-only devices use your product. An estimated 1 in 6 people worldwide experience significant disability — and everyone benefits from clear structure, keyboard support, and readable text.
You already have a dedicated guide for WCAG color contrast and a Color Contrast Checker. This article covers everything else that makes interfaces perceivable, operable, understandable, and robust — the four WCAG principles — with patterns you can ship this week.
Bottom line: Start with semantic HTML and keyboard access. Add ARIA only when native elements cannot express behavior. Test with a keyboard and one screen reader before claiming compliance. Contrast is necessary; it is not sufficient.
Why accessibility matters for developers
| Reason |
Detail |
| Legal |
EU Accessibility Act (2025), ADA lawsuits, Section 508 for US public sector |
| SEO |
Semantic HTML and clear headings help crawlers and AI summaries |
| Quality |
Accessible components tend to be more testable and resilient |
| Reach |
Temporary injuries, aging eyes, mobile one-handed use |
| Business |
Larger addressable market; fewer support tickets from confused UX |
Accessibility failures are usually implementation bugs, not design philosophy debates. Fix the bug.
The WCAG framework (developer view)
WCAG 2.2 organizes requirements into four principles — POUR:
| Principle |
Question it answers |
Dev examples |
| Perceivable |
Can users sense the content? |
Alt text, captions, contrast, resize text |
| Operable |
Can users interact? |
Keyboard, no seizure triggers, enough time |
| Understandable |
Can users comprehend? |
Labels, errors, consistent navigation |
| Robust |
Does it work with assistive tech? |
Valid HTML, name/role/value for custom widgets |
Most teams target Level AA — the baseline referenced by regulations and procurement checklists.
Levels at a glance
| Level |
Meaning |
| A |
Minimum; many failures still possible |
| AA |
Industry standard for compliance |
| AAA |
Strict; often reserved for specialized content |
Do not chase AAA everywhere — aim AA globally, AAA where it matters (critical flows, long-form reading).
Layer 1: Semantic HTML (the free wins)
Native HTML elements carry built-in accessibility. Browsers and assistive technologies already know what a <button> does. A <div onclick> does not — until you bolt on tabindex, keyboard handlers, and ARIA, and still get edge cases wrong.
Prefer native elements
| Need |
Use |
Avoid |
| Click action |
<button type="button"> |
<div> / <span> with click handler |
| Navigation |
<a href="..."> |
<div> with router click |
| Text input |
<input>, <textarea>, <select> |
Unlabeled custom fields |
| Page sections |
<header>, <main>, <nav>, <footer> |
<div class="header"> soup |
| Lists |
<ul>, <ol>, <li> |
Divs with bullet CSS |
| Tables (data) |
<table>, <th scope> |
CSS grid pretending to be a table |
Document outline
One <h1> per page (usually the page title). Headings descend in order — do not skip from <h2> to <h5> because CSS made it look fine.
<h1>JSON Formatter</h1>
<h2>Paste your JSON</h2>
<h2>Output</h2>
<h2>FAQ</h2>
Screen reader users navigate by heading — treat them like a table of contents.
Language
Set lang on <html>:
<html lang="en">
For inline foreign phrases: <span lang="pt">obrigado</span>. Screen readers switch pronunciation.
Review markup in the workflow
When auditing pasted HTML from CMS exports or email templates, format and inspect structure with the HTML Beautifier. Missing <label>, orphan <div> buttons, and empty headings show up faster in readable markup.
Layer 2: Keyboard accessibility
If it works with a mouse but not Tab, Enter, Space, and Escape, it is broken for many users — power users, motor disabilities, and broken trackpads included.
Minimum keyboard checklist
Focus visibility (WCAG 2.4.7)
Focus rings must be visible. Common failure: outline: none without a replacement.
:focus-visible {
outline: 2px solid var(--ring);
outline-offset: 2px;
}
Use :focus-visible so mouse clicks do not show a ring on every button click — keyboard users still get a clear indicator.
Skip links
<a href="#main-content" class="sr-only focus:not-sr-only">
Skip to main content
</a>
<main id="main-content">...</main>
Sighted keyboard users tab past chrome; screen reader users jump straight to content.
Custom components
Dropdowns, tabs, comboboxes, and modals need keyboard patterns from WAI-ARIA Authoring Practices:
| Widget |
Expected keys |
| Tabs |
Arrow keys switch tabs; Tab exits widget |
| Menu |
Arrow keys navigate; Escape closes |
| Dialog |
Focus trapped inside; Escape closes; focus returns on close |
| Combobox |
Type to filter; Arrow down opens list |
Do not invent behavior — copy the APG pattern and test it.
Layer 3: ARIA — use sparingly
ARIA (Accessible Rich Internet Applications) adds semantics when HTML alone is insufficient. First rule of ARIA:
No ARIA is better than bad ARIA.
When ARIA helps
| Scenario |
Example |
| Custom tab panel |
role="tablist", role="tab", aria-selected |
| Live region updates |
aria-live="polite" on toast container |
| Icon-only button |
aria-label="Close dialog" |
| Loading state |
aria-busy="true" on updating region |
| Described-by errors |
aria-describedby="email-error" |
When ARIA hurts
| Anti-pattern |
Why |
role="button" on div without keyboard support |
Announces as button, behaves like dead div |
aria-label overriding visible text with different words |
Confuses voice control users ("Click Submit" fails) |
| Redundant roles |
<button role="button"> — noise |
aria-hidden="true" on focusable child |
Focus disappears into hidden content |
| Labels on everything |
Over-announcement fatigue |
Accessible name calculation
Assistive tech exposes a computed name from (simplified priority):
aria-labelledby
aria-label
- Associated
<label>
- Element text content
title attribute (unreliable — do not depend on it)
Rule: Visible label text should match the accessible name when possible — critical for voice control ("Click JSON Formatter").
Forms are where accessibility lawsuits and abandoned checkouts cluster.
<!-- Good -->
<label for="json-input">JSON input</label>
<textarea id="json-input" name="json"></textarea>
<!-- Acceptable visually hidden label -->
<label for="search" class="sr-only">Search tools</label>
<input id="search" type="search" />
Placeholder is not a label — low contrast, disappears on type, not announced reliably.
<fieldset>
<legend>Export format</legend>
<label><input type="radio" name="fmt" value="json" /> JSON</label>
<label><input type="radio" name="fmt" value="csv" /> CSV</label>
</fieldset>
Error messages (WCAG 3.3.1, 3.3.3)
| Requirement |
Implementation |
| Identify the field in error |
aria-invalid="true" |
| Describe the error in text |
Visible message + aria-describedby="field-error-id" |
| Suggest correction when possible |
"Use ISO 8601 date format (YYYY-MM-DD)" |
<label for="email">Email</label>
<input
id="email"
type="email"
aria-invalid="true"
aria-describedby="email-error"
/>
<p id="email-error" role="alert">Enter an email with @ symbol.</p>
Use role="alert" or aria-live="assertive" for dynamic errors so screen readers announce them.
Autocomplete
Use autocomplete attributes on common fields (email, name, street-address) — helps password managers and users with cognitive disabilities.
| Content type |
What to do |
| Informative image |
Meaningful alt describing purpose |
| Decorative image |
alt="" (empty, not missing) |
| Icon with action |
aria-label or visible text + aria-hidden on icon SVG |
| Icon decorative next to text |
aria-hidden="true" on icon |
| Complex chart |
Short alt + longer description nearby or aria-describedby |
| Video |
Captions; transcript for audio-only |
SVG icons
<button type="button" aria-label="Copy to clipboard">
<svg aria-hidden="true" focusable="false">...</svg>
</button>
For entity and symbol work in content, the HTML Entity Lookup helps verify character references render correctly across browsers — broken entities become tofu boxes that fail both readability and screen reader pronunciation.
Layer 6: Motion, animation, and vestibular disorders
WCAG 2.3.3 Animation from Interactions (AA): motion triggered by interaction can be disabled unless essential.
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
Also respect prefers-reduced-motion in JavaScript for scroll-driven animations and carousels.
Never autoplay video with sound. Pause/stop/hide controls for any moving content over 5 seconds (WCAG 2.2.2).
Layer 7: Color — do not stop at contrast
Color alone must not convey meaning (WCAG 1.4.1). Red/green error states need icons or text as well.
| Failure |
Fix |
| "Green row = success, red = fail" only |
Add ✓ / ✗ icon or "Success" / "Error" text |
| Required field marked with red asterisk only |
Add "(required)" in label |
| Chart series distinguished by hue only |
Patterns, labels, or direct annotations |
Deep dive: WCAG Color Contrast guide and the Color Contrast Checker.
Layer 8: Dynamic content and SPAs
Single-page apps and client-heavy tools (like online formatters) introduce unique gaps.
Route changes
Announce page changes when the URL updates without full reload:
<h1 aria-live="polite" tabindex="-1" ref={headingRef}>
JSON Formatter
</h1>
Move focus to the heading on navigation so screen reader users know the context changed.
Live regions
| Politeness |
Use |
aria-live="polite" |
Status updates ("Copied to clipboard") |
aria-live="assertive" |
Urgent errors |
aria-live="off" |
Decorative ticker text |
Do not overuse — constant announcements overwhelm users.
Loading states
- Show visible loading indicator
- Use
aria-busy="true" on updating container
- Avoid infinite spinners with no timeout or error path
1. Keyboard-only pass (10 minutes)
Unplug the mouse. Tab through every page. Can you:
- Reach all actions?
- See focus?
- Operate dialogs and menus?
- Submit forms and read errors?
2. Screen reader smoke test (30 minutes)
Pick one:
- Windows: NVDA (free) + Firefox or Chrome
- macOS: VoiceOver (built-in) + Safari
- Mobile: TalkBack (Android) or VoiceOver (iOS)
Navigate by headings and landmarks. Does the structure make sense?
3. Automated scan (CI-friendly)
Tools like axe-core, Lighthouse accessibility audit, or eslint-plugin-jsx-a11y catch ~30–40% of issues — mostly missing labels, contrast, and duplicate IDs. They do not replace manual testing.
4. Zoom and reflow
- Zoom to 200% — content should reflow without horizontal scroll (WCAG 1.4.10)
- Test at 320px viewport width
5. Content sanity
Markdown and docs rendered to HTML should preserve heading hierarchy. Preview rendered output with the Markdown Preview tool before publishing blog posts — broken heading levels in source become broken outline for assistive tech.
Accessibility checklist for shipping a feature
Copy into PR descriptions:
| Failure |
Why it hurts |
Fix |
| Code editor not announced |
Screen reader users cannot use the core product |
Label the region; document keyboard shortcuts |
| Copy button icon-only |
No accessible name |
aria-label="Copy output" |
| Toast disappears too fast |
Users miss confirmation |
aria-live + sufficient display time |
| Split pane drag handle too small |
Motor impairment |
Min 44×44px touch target (WCAG 2.5.8 target size) |
| Theme toggle without label |
Unknown purpose |
aria-label="Toggle dark mode" |
| Results only shown in color |
Color-blind users miss errors |
Text + icon for error/success |
Privacy-first tools that process data client-side still owe operable, perceivable UI — local processing does not exempt the interface.
React / Next.js patterns (quick reference)
| Pattern |
Approach |
| Client-only widgets |
Load with care; provide static fallback content where possible |
| Modals |
Focus trap, role="dialog", aria-modal="true", return focus on close |
| Radix / shadcn |
Built on accessible primitives — do not strip labels when composing |
suppressHydrationWarning on body |
OK for extensions; do not hide real content mismatches from users |
| Icon components |
Pass aria-hidden when decorative |
Prefer library primitives that ship APG-aligned behavior over custom div stacks.
Prioritize by impact
If the backlog is infinite, fix in this order:
- Keyboard blockers — unreachable features
- Missing form labels and error text
- Contrast failures on primary text and buttons
- Focus indicators removed by global CSS resets
- Custom widget keyboard support
- Heading structure on marketing and docs pages
- Fine-grained ARIA polish
Use these codefunc tools alongside this guide:
Practical takeaway
Accessible web development is mostly good HTML, keyboard support, and clear text — not a separate accessibility sprint. Build semantic structure first. Test with Tab and one screen reader. Measure contrast with real pairs and both themes. Use ARIA to fill gaps, not to patch bad markup.
Ship features everyone can use. Regulations follow; users lead.
Sources