Make your website accessible to everyone. Learn WCAG 2.1 AA compliance, color contrast ratios, keyboard navigation, screen readers, and ARIA best practices.
One billion people worldwide live with some form of disability. That is roughly 15% of the global population. When your website is not accessible, you are locking out a significant portion of your potential audience — not because they cannot use the web, but because your site will not let them.
Web accessibility is not a nice-to-have feature you bolt on after launch. It is a fundamental quality of well-built software. And in 2026, with the European Accessibility Act now in effect and ADA lawsuits hitting record numbers, it is also a legal requirement for most commercial websites.
I spent months bringing akousa.net to full WCAG 2.1 AA compliance — over 100 files modified across 20 locales, covering 15 separate success criteria. This guide distills everything I learned into practical, actionable steps you can apply to your own projects today.
WCAG is built on four foundational principles, abbreviated as POUR. Every success criterion maps back to one of these:
Perceivable — Information must be presentable in ways users can perceive. A blind user cannot see an image, so you provide alt text. A deaf user cannot hear audio, so you provide captions.
Operable — Interface components must be operable by all users. If a sighted user can click a button, a keyboard user must be able to activate it too. If a mouse user can hover over a tooltip, a screen reader user needs an equivalent way to access that content.
Understandable — Content and interface behavior must be understandable. Error messages should explain what went wrong and how to fix it. Navigation should be consistent across pages. Language changes within content should be marked up so screen readers switch pronunciation.
Robust — Content must be robust enough to be interpreted by a wide variety of user agents, including assistive technologies. This means valid HTML, proper use of ARIA, and semantic markup that machines can parse reliably.
Every accessibility fix you make falls under one of these four pillars. When you are unsure whether something is an accessibility issue, ask yourself: "Does this violate Perceivable, Operable, Understandable, or Robust?" If yes, fix it.
Color contrast failures account for more WCAG violations than any other single criterion. The rules are straightforward:
Here is what a contrast check looks like in practice:
/* Fails — ratio is 2.4:1 */
.low-contrast {
color: #999999;
background-color: #ffffff;
}
/* Passes — ratio is 7.0:1 */
.high-contrast {
color: #595959;
background-color: #ffffff;
}
/* Passes for large text — ratio is 3.1:1 */
.large-text-ok {
color: #767676;
background-color: #ffffff;
font-size: 18px;
}Do not guess. Use a Color Contrast Checker to verify every text-background combination in your design system. When building new color palettes, run them through a Color Palette Generator that surfaces contrast ratios so you catch problems before they reach code.
One mistake I see constantly: developers check contrast in their light theme and forget to check dark mode. Both themes need to pass independently. Your dark mode might have light gray text on a dark gray background that looks fine to you but fails the 4.5:1 ratio.
Every interactive element on your page must be reachable and operable with a keyboard alone. No exceptions. Many users with motor disabilities cannot use a mouse. Screen reader users navigate entirely by keyboard. Power users prefer keyboard shortcuts.
The core requirements:
Tab order must be logical. It should follow the visual reading order — left to right, top to bottom in LTR languages. Do not use positive tabindex values to force a custom order. Fix your DOM order instead.
Focus must be visible. The default browser focus outline works. If your designer wants to remove it, replace it with something equally visible — never remove it without a substitute.
All interactive elements must be focusable. Buttons, links, form fields — these are focusable by default. But if you built a custom dropdown with <div onclick="...">, it is invisible to keyboard users.
No keyboard traps. A user must be able to navigate into and out of every component using standard keys (Tab, Shift+Tab, Escape, Arrow keys).
// Bad: div pretending to be a button
<div onClick={handleClick} className="btn">
Submit
</div>
// Good: actual button element
<button onClick={handleClick} className="btn">
Submit
</button>
// Good: custom component with proper keyboard support
<div
role="button"
tabIndex={0}
onClick={handleClick}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleClick();
}
}}
className="btn"
>
Submit
</div>The second approach — using a <div> with role="button" — works but is almost never necessary. The native <button> element gives you keyboard handling, focus management, and screen reader announcements for free. Use semantic HTML first. Reach for ARIA only when semantic HTML cannot express what you need.
SPAs break the browser's natural focus management. When a user clicks a link and the page content changes without a full reload, focus stays wherever it was — often on a now-invisible element. Screen reader users hear nothing. They do not know the page changed.
Fix this by managing focus on route changes:
import { useEffect, useRef } from "react";
import { usePathname } from "next/navigation";
function MainContent({ children }: { children: React.ReactNode }) {
const mainRef = useRef<HTMLElement>(null);
const pathname = usePathname();
useEffect(() => {
// Move focus to main content on route change
mainRef.current?.focus();
}, [pathname]);
return (
<main ref={mainRef} tabIndex={-1} id="main-content">
{children}
</main>
);
}The tabIndex={-1} makes the element programmatically focusable without adding it to the tab order. This pattern ensures screen reader users know when content has changed.
Also implement skip links. The very first focusable element on every page should be a "Skip to main content" link that jumps past your navigation:
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:top-4
focus:left-4 focus:z-50 focus:bg-white focus:px-4
focus:py-2 focus:text-black"
>
Skip to main content
</a>This link is invisible until a keyboard user presses Tab. Then it appears, and pressing Enter jumps them straight to the content.
Screen readers convert visual interfaces into audio. They read text, announce element roles ("button", "link", "heading level 2"), and convey states ("expanded", "selected", "checked"). For this to work, your HTML must carry semantic meaning.
Use HTML5 landmark elements so screen reader users can jump between sections:
<header>
<!-- banner landmark -->
<nav>
<!-- navigation landmark -->
<main>
<!-- main landmark -->
<aside>
<!-- complementary landmark -->
<footer><!-- contentinfo landmark --></footer>
</aside>
</main>
</nav>
</header>If you have multiple <nav> elements (primary navigation, footer navigation, breadcrumbs), label them:
<nav aria-label="Primary navigation">...</nav>
<nav aria-label="Breadcrumb">...</nav>
<nav aria-label="Footer navigation">...</nav>When content updates dynamically — a form error appears, a notification pops up, a score changes — screen readers will not announce it unless you tell them to:
// Polite: waits until screen reader finishes current speech
<div aria-live="polite" role="status">
{statusMessage}
</div>
// Assertive: interrupts current speech (use sparingly)
<div aria-live="assertive" role="alert">
{errorMessage}
</div>Most ARIA usage I see in the wild is incorrect. The first rule of ARIA is: do not use ARIA if native HTML can do the job. That said, here are the attributes you will genuinely need:
aria-label — Labels an element when visible text is not present (icon buttons, for example)aria-labelledby — Points to another element that serves as the labelaria-describedby — Points to supplementary description textaria-expanded — Communicates whether a collapsible section is open or closedaria-hidden="true" — Hides decorative elements from screen readersaria-live — Marks a region for dynamic content announcementsaria-current="page" — Marks the current page in navigation// Icon button without visible text
<button aria-label="Close dialog" onClick={onClose}>
<XIcon aria-hidden="true" />
</button>
// Accordion with proper state
<button
aria-expanded={isOpen}
aria-controls="panel-1"
onClick={() => setIsOpen(!isOpen)}
>
Frequently Asked Questions
</button>
<div id="panel-1" role="region" hidden={!isOpen}>
{content}
</div>Every <img> needs an alt attribute. But not every image needs descriptive alt text.
Informative images get descriptive alt text that conveys the same information the image does:
<img
src="/chart.png"
alt="Bar chart showing page load times decreasing
from 4.2 seconds in January to 1.8 seconds in March after optimization"
/>Decorative images get empty alt text so screen readers skip them:
<img src="/decorative-divider.svg" alt="" />Images of text should be avoided entirely. Use real text styled with CSS. If you must use an image of text, the alt text must contain the exact same text.
Complex images (charts, diagrams, infographics) need both a brief alt text and a longer 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 PostgreSQL for persistent storage and Redis for caching and session management.
</figcaption>
</figure>Forms are where accessibility most directly impacts whether someone can actually use your product. A beautiful form that a screen reader cannot parse is a locked door.
// Every input needs a label
<div>
<label htmlFor="email">Email address</label>
<input
id="email"
type="email"
aria-required="true"
aria-invalid={!!errors.email}
aria-describedby={errors.email ? "email-error" : "email-hint"}
/>
<span id="email-hint" className="text-sm text-gray-500">
We will never share your email.
</span>
{errors.email && (
<span id="email-error" role="alert" className="text-sm text-red-600">
{errors.email}
</span>
)}
</div>Key form accessibility patterns:
<label htmlFor="id"> or aria-label. Placeholder text is not a label — it disappears when the user types.role="alert" or aria-live="assertive" on error containers.aria-required="true" and a visual indicator (not color alone).<fieldset> and <legend> for radio buttons and checkbox groups.autocomplete to common fields (name, email, address, credit card) so browsers and password managers can autofill them.Before you add a single ARIA attribute, make sure your HTML is semantic. Proper heading hierarchy alone fixes a remarkable number of accessibility issues:
<!-- Bad: heading levels skip around -->
<h1>Page Title</h1>
<h3>First Section</h3>
<!-- skipped h2 -->
<h5>Subsection</h5>
<!-- skipped h4 -->
<!-- Good: heading levels are sequential -->
<h1>Page Title</h1>
<h2>First Section</h2>
<h3>Subsection</h3>Screen reader users navigate by headings more than any other method. Skipped heading levels confuse the document outline and make navigation unpredictable.
Use the right element for the job:
| Need | Element | Not This |
|---|---|---|
| Navigation link | <a href="..."> | <span onClick> |
| Action button | <button> | <div onClick> |
| Data table | <table> with <th> | Nested <div>s |
| List of items | <ul> or <ol> | Series of <div>s |
| Section heading | <h2> through <h6> | Bold <p> |
Automated tools catch approximately 30-40% of accessibility issues. The rest require manual testing. Use both.
Run your site through a Website Analyzer that includes accessibility audits. For development, integrate these into your workflow:
# Install the ESLint plugin
npm install eslint-plugin-jsx-a11y --save-devprefers-reduced-motion?@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}Here is something that surprises many developers: most accessibility improvements also boost SEO. Search engines and screen readers parse pages in similar ways. Both rely on semantic HTML, heading hierarchy, alt text, link text, and structured metadata.
Proper heading hierarchy helps search engines understand your content structure. Descriptive alt text helps image search indexing. Semantic HTML helps crawlers parse your page. A Meta Tag Generator can help you ensure your pages have proper title tags, descriptions, and Open Graph data — which benefit both search visibility and assistive technology.
The overlap is significant enough that if you are doing accessibility right, you are likely doing SEO right too. And vice versa.
Even with better tooling and awareness, these mistakes persist:
Using outline: none without a replacement. This removes the focus indicator for keyboard users. Always provide an alternative focus style.
Color as the only indicator. Red for errors, green for success — users with color blindness cannot distinguish them. Add icons, text labels, or patterns.
Auto-playing media. Video or audio that plays automatically without user consent is disorienting, especially for screen reader users whose audio output is their primary interface.
Missing language attribute. Without <html lang="en">, screen readers may use the wrong pronunciation engine for your content.
Inaccessible modals. Modals must trap focus, be dismissable with Escape, and return focus to the trigger element when closed.
Infinite scroll without alternatives. Users who navigate by keyboard or screen reader may never reach your footer content. Provide pagination or a "Load more" button as an alternative.
If your site has zero accessibility work done, do not try to fix everything at once. Start with the highest-impact changes:
lang attribute to your <html> elementalt textAccessibility is not a one-time project. It is an ongoing practice, like security or performance. Every new feature you build should be accessible from the start. Every pull request should include accessibility as part of code review.
The web was designed to be universal. Accessibility is how we keep that promise.