Test your own site
Run a quick automated check against any public URL using free accessibility tools.
Or install the axe DevTools browser extension (opens in new tab) to test any site directly in your browser's DevTools.
Color Contrast
Text must have a contrast ratio of at least 4.5:1 against its background (3:1 for large text). Low-contrast text fails users with low vision, in bright sunlight, or on low-quality screens.
color: #9ca3af on #ffffff
The quick brown fox jumps over the lazy dog. This text is styled with low contrast gray — it looks subtle, but it fails WCAG.
Contrast ratio: 2.3:1 Fails AA
color: #374151 on #ffffff
The quick brown fox jumps over the lazy dog. This text uses sufficient contrast — readable for everyone.
Contrast ratio: 10.7:1 Passes AAA
Try it: Live Contrast Checker
- Contrast ratio
- 10.7:1
- Normal text (AA)
- Pass
- Large text (AA)
- Pass
View code
Broken
/* Insufficient contrast */
.text {
color: #9ca3af;
background: #ffffff;
/* Ratio: 2.3:1 — Fails AA */
}
Fixed
/* Sufficient contrast */
.text {
color: #374151;
background: #ffffff;
/* Ratio: 10.7:1 — Passes AAA */
}
Alt Text
Every meaningful image needs a text alternative that conveys the same information.
Decorative images should use alt="" so screen readers skip them entirely.
Informative image — missing alt
Functional image — meaningless alt
Decorative image — redundant alt
Informative image — descriptive alt
Functional image — action-based alt
Decorative image — empty alt
View code
Broken
<!-- Missing alt -->
<img src="chart.png">
<!-- Filename as alt -->
<button>
<img src="search.png"
alt="search.png">
</button>
<!-- Noisy decorative alt -->
<img src="divider.png"
alt="squiggly line">
Fixed
<!-- Descriptive alt -->
<img src="chart.png"
alt="Bar chart: Q4 revenue up 23%">
<!-- Alt describes the action -->
<button>
<img src="search.png"
alt="Search products">
</button>
<!-- Empty alt for decorative -->
<img src="divider.png" alt="">
Form Labels
Placeholder text is not a label. It disappears on focus, leaving users without context.
Every input needs a persistently visible, programmatically associated <label>.
View code
Broken
<!-- No label element -->
<input
type="email"
placeholder="Email">
Fixed
<!-- Visible, associated label -->
<label for="email">
Email address
</label>
<input
type="email"
id="email"
placeholder="e.g. jane@example.com">
Focus Visibility
Removing the focus outline is one of the most common and harmful accessibility mistakes.
Keyboard users rely on visible focus to navigate. Use :focus-visible
to show focus rings for keyboard navigation without affecting mouse users.
outline: none
Tab through these links with your keyboard. Where is the focus?
Focus is invisible — keyboard users are lost.
:focus-visible
Tab through these links — a clear focus ring follows your position.
Tab to see which link has focus.
View code
Broken
/* Removes focus for everyone */
a:focus {
outline: none;
}
Fixed
/* Only shows for keyboard nav */
a:focus-visible {
outline: 3px solid #ca8a04;
outline-offset: 3px;
border-radius: 2px;
}
Heading Structure
Screen reader users navigate pages by jumping between headings — it's their table of contents. Skipped levels and non-semantic markup break that navigation entirely.
Document outline (screen reader sees):
- h1: Page Title
- h4: Section A ⚠ skipped h2, h3
- h2: Section B
- h5: Subsection ⚠ skipped h3, h4
- div: "Section C" ⚠ not a heading at all
Document outline (screen reader sees):
- h1: Page Title
- h2: Section A
- h3: Subsection A.1
- h2: Section B
- h3: Subsection B.1
- h2: Section C
View code
Broken
<h1>Page Title</h1>
<!-- jumped from h1 to h4 -->
<h4>Section A</h4>
<h2>Section B</h2>
<!-- not a heading at all -->
<div class="big-text">Section C</div>
Fixed
<h1>Page Title</h1>
<h2>Section A</h2>
<h3>Subsection A.1</h3>
<h2>Section B</h2>
<h3>Subsection B.1</h3>
<h2>Section C</h2>
Button Semantics
Using <div> or <span> as clickable controls is a
common anti-pattern. They're invisible to keyboards and screen readers unless you manually
recreate what <button> gives you for free.
<div> as button
Try to Tab to the buttons below. Then try clicking one.
<button>
Tab to these buttons, then activate with Enter or Space.
View code
Broken
<!-- Not keyboard accessible -->
<!-- No role, no name, no focus -->
<div onclick="addToCart()">
Add to Cart
</div>
Fixed
<!-- Keyboard, role, name: all free -->
<button type="button"
onclick="addToCart()">
Add to Cart
</button>
Modal Focus Trap
When a modal dialog opens, keyboard focus must be trapped inside it. Focus should not leak into background content. When closed, focus must return to the element that opened it.
Open the modal and press Tab repeatedly — focus will escape into the page behind it.
Open the modal, Tab through it — focus stays inside. Press Escape or Close to dismiss and return focus.
Subscribe to newsletter
Enter your email to get updates.
Focus is not trapped — Tab past "Subscribe" and focus leaves the modal.
Subscribe to newsletter
Enter your email to get updates.
Focus is trapped inside. Press Escape or Cancel to close and return focus to the trigger button.
View code
Broken
<!-- div, no ARIA, no trap -->
<div class="modal">
...content...
</div>
Fixed
<div role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
tabindex="-1">
<h2 id="modal-title">...</h2>
...content...
</div>
<!-- JS traps focus + handles Escape -->
<!-- inert attr applied to siblings -->
Live Regions
When content updates dynamically — search results, form errors, loading states — screen
readers need to be told. Without aria-live, these updates are silent.
Click "Save" — visually the status updates, but a screen reader hears nothing.
role="status" announces
Click "Save" — the update is announced to screen readers via a live region.
View code
Broken
<!-- No live region -->
<div id="status"></div>
// JS updates visually only
status.textContent = 'Saved!';
Fixed
<!-- Announced on change -->
<div id="status"
role="status"
aria-live="polite"
aria-atomic="true">
</div>
// Same JS update — now announced
status.textContent = 'Settings saved.';
Skip Link
Without a skip link, keyboard users must Tab through every navigation item on every page load before reaching content. A single visually-hidden link at the top of the page solves this. This page's own skip link is the live demonstration — press Tab on page load.
Simulated keyboard navigation without a skip link:
6 Tab presses to reach content on every page load
Simulated keyboard navigation with a skip link:
2 Tab presses — press Enter on the skip link to jump directly.
Try it on this page: scroll back to the top and press Tab — the "Skip to main content" link will appear.
View code
Broken
<body>
<!-- No skip link -->
<nav>...10 links...</nav>
<main>Content</main>
</body>
Fixed
<body>
<!-- First focusable element -->
<a href="#main" class="skip-link">
Skip to main content
</a>
<nav>...10 links...</nav>
<main id="main">Content</main>
</body>
/* Visible only on focus */
.skip-link {
position: absolute;
transform: translateY(-100%);
}
.skip-link:focus {
transform: translateY(0);
}
Reduced Motion
Persistent, large-scale motion can trigger vestibular disorders — causing nausea,
dizziness, and migraines. Respecting prefers-reduced-motion is a
straightforward, high-impact fix. Listed as AAA, but failure has real physical consequences.
This animation plays regardless of the user's OS motion setting.
prefers-reduced-motion
Animation respects your OS preference. Use the toggle below to simulate.
View code
Broken
/* Animation always plays */
.box {
animation: spin 2s
linear infinite;
}
Fixed
/* Animation plays by default */
.box {
animation: spin 2s
linear infinite;
}
/* Removed when user prefers it */
@media (prefers-reduced-motion: reduce) {
.box {
animation: none;
}
}
Carousel
Auto-advancing carousels must provide a mechanism to pause, stop, or hide the movement. Without one, users who need more time to read — or who find motion distracting — have no recourse. Keyboard users also need explicit controls to navigate between slides.
Watch the slides advance every 3 seconds. There is no way to pause, and no keyboard controls to change slides.
Use Prev/Next or the pause button to navigate. Focusing any control automatically pauses the auto-advance.
View code
Broken
<!-- div, no controls, no ARIA -->
<div class="carousel">
<div class="slide">...</div>
<div class="slide">...</div>
</div>
<!-- JS auto-advances, no pause -->
Fixed
<div role="region"
aria-label="A11y tips"
aria-roledescription="carousel">
<div aria-live="off"
aria-atomic="true">
<div role="group"
aria-roledescription="slide"
aria-label="Slide 1 of 3">
</div>
</div>
<button aria-label="Pause"
aria-pressed="false"
type="button">⏸</button>
</div>
Tooltip
Custom tooltips are often triggered only on mouse hover — keyboard users who Tab
to an icon-only button never see the tooltip and can't tell what the control does.
The fix triggers tooltips on both hover and focus, associates them via
aria-describedby, and lets users dismiss them with Escape.
Hover the buttons to see tooltips. Tab to them with a keyboard — no tooltip appears and there is no accessible name.
Hover or Tab to the buttons — the tooltip appears either way. Press Escape to dismiss it.
View code
Broken
<!-- no accessible name, hover-only -->
<button type="button">
<svg ...></svg>
</button>
<div class="tooltip">Save</div>
<!-- .tooltip shown via CSS :hover
on parent — never on :focus -->
Fixed
<button type="button"
aria-describedby="tip-save">
<svg ...></svg>
<span class="sr-only">Save</span>
</button>
<div role="tooltip"
id="tip-save" hidden>
Save document
</div>
<!-- JS shows on hover + focus;
Escape dismisses -->