Web accessibility isn't about compliance checkboxes. It's about building interfaces that work for everyone — including the 15% of the global population with disabilities, users on slow connections, people using their phone one-handed, and anyone who's ever tried to use a website in bright sunlight.
At Pillai Infotech, we learned this the hard way. A client's e-commerce site was losing 12% of checkout completions because the payment form was unusable with screen readers. Fixing accessibility didn't just help disabled users — it improved the experience for everyone and increased conversions by 8%.
Why Accessibility Matters More Than Ever
The Business Case
- Legal risk is real. Web accessibility lawsuits hit 4,600+ in the US in 2025 alone. The EU's European Accessibility Act takes effect in June 2025, covering all digital products sold in Europe.
- Market size. 1.3 billion people worldwide have significant disabilities. That's a market larger than China. Add temporary and situational disabilities (broken arm, holding a baby, noisy environment) and you're talking about every user at some point.
- SEO boost. Accessible sites rank better. Semantic HTML, alt text, heading hierarchy, and fast load times are both accessibility features and SEO signals. Google explicitly considers page experience metrics that overlap with accessibility.
The Engineering Case
Accessible code is usually better code. When you build with semantic HTML, proper labels, and keyboard support, you get:
- Easier automated testing (proper labels = easy selectors)
- Better mobile experience (touch targets, readable text)
- Improved performance (semantic HTML is lighter than div soup)
- Fewer bugs (proper state management for interactive elements)
WCAG 2.2 Conformance Levels
WCAG organizes requirements into three conformance levels. Understanding which level you need prevents both under-engineering and over-engineering.
| Level | What It Means | Who Needs It | Effort |
|---|---|---|---|
| A | Minimum — removes biggest barriers | Bare minimum for any public site | Low |
| AA | Standard — good experience for most users | Legal standard in most jurisdictions. Aim here. | Moderate |
| AAA | Enhanced — highest accessibility | Government, healthcare, education | High |
Our recommendation: Target WCAG 2.2 Level AA. It's the legal standard in most countries, covers the vast majority of accessibility needs, and is achievable without excessive effort if you bake it into your development process from the start.
Semantic HTML: The Foundation You're Probably Skipping
Before reaching for ARIA attributes, fix your HTML. Semantic elements give you accessibility for free — screen readers, keyboard navigation, and browser features that just work.
<div class="header">
<div class="nav">
<div class="nav-item" onclick="navigate('/about')">About</div>
</div>
</div>
<div class="main">
<div class="title">Our Services</div>
<div class="btn" onclick="submit()">Submit</div>
</div>
<header>
<nav aria-label="Main navigation">
<a href="/about">About</a>
</nav>
</header>
<main>
<h1>Our Services</h1>
<button type="submit">Submit</button>
</main>
The semantic version gives you:
- Landmarks — screen readers can jump between header, nav, main, footer
- Keyboard support —
<a>and<button>are focusable and activatable by default - Heading hierarchy — screen readers can navigate by heading level
- Form semantics —
<button type="submit">works with Enter key automatically
The Heading Hierarchy Rule
Headings must follow a logical order: H1 → H2 → H3. Never skip levels (H1 → H3). Think of it like a document outline — screen reader users navigate by headings the way sighted users scan a page visually.
<h1>Web Development Services</h1>
<h3>Frontend Development</h3>
<h4>React Applications</h4>
<h1>Web Development Services</h1>
<h2>Frontend Development</h2>
<h3>React Applications</h3>
<h3>Vue Applications</h3>
<h2>Backend Development</h2>
<h3>Node.js APIs</h3>
ARIA: Powerful but Dangerous
ARIA (Accessible Rich Internet Applications) lets you add accessibility information to custom widgets. But the first rule of ARIA is: don't use ARIA if a native HTML element does the job.
When ARIA Is Necessary
- Custom widgets — tabs, accordions, modals, comboboxes that HTML doesn't provide natively
- Dynamic content — live regions that announce updates to screen readers
- Relationships — connecting labels to controls, describing errors
Common ARIA Patterns
<div aria-live="polite" aria-atomic="true">
3 items in your cart
</div>
<div role="dialog" aria-modal="true"
aria-labelledby="modal-title">
<h2 id="modal-title">Confirm Deletion</h2>
<p>This action cannot be undone.</p>
<button>Delete</button>
<button>Cancel</button>
</div>
<div role="tablist" aria-label="Account settings">
<button role="tab" aria-selected="true"
aria-controls="panel-1" id="tab-1">
Profile
</button>
<button role="tab" aria-selected="false"
aria-controls="panel-2" id="tab-2">
Security
</button>
</div>
<div role="tabpanel" id="panel-1"
aria-labelledby="tab-1">
Profile content here...
</div>
role="button" to a <div> doesn't make it a button. You still need tabindex="0", keyboard event handlers (Enter and Space), and focus styling. Just use <button> — you get all of this free.
Keyboard Navigation: The Most Overlooked Requirement
Every interactive element must be usable with a keyboard alone. This helps screen reader users, power users, people with motor impairments, and anyone whose mouse just died.
Essential Keyboard Patterns
- Tab — moves focus to the next interactive element
- Shift+Tab — moves focus backward
- Enter/Space — activates buttons and links
- Escape — closes modals, dropdowns, popups
- Arrow keys — navigates within widgets (tabs, menus, radio groups)
Focus Management
/* Visible focus indicator — REQUIRED */
/* Never use outline: none without a replacement! */
:focus-visible {
outline: 2px solid #4f46e5;
outline-offset: 2px;
}
/* Skip link — lets keyboard users bypass navigation */
.skip-link {
position: absolute;
top: -40px;
left: 0;
padding: 8px 16px;
background: #4f46e5;
color: white;
z-index: 100;
}
.skip-link:focus {
top: 0;
}
<!-- Skip link in HTML -->
<a class="skip-link" href="#main-content">
Skip to main content
</a>
<nav>...long navigation...</nav>
<main id="main-content">
<!-- Page content -->
</main>
Focus Trapping in Modals
When a modal opens, focus must be trapped inside it — Tab should cycle through the modal's interactive elements, not escape to the page behind. When the modal closes, return focus to the element that triggered it.
// Focus trap for modal dialogs
function trapFocus(modal) {
const focusable = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
modal.addEventListener('keydown', (e) => {
if (e.key !== 'Tab') return;
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
});
first.focus(); // Move focus into modal on open
}
Visual Design and Color Accessibility
Color Contrast Requirements
WCAG AA requires a contrast ratio of at least 4.5:1 for normal text and 3:1 for large text (18px+ bold or 24px+ regular). This isn't arbitrary — it's the threshold where text becomes readable for people with moderate visual impairments.
| Element | AA Minimum | AAA Target | Quick Check |
|---|---|---|---|
| Normal text (<18px) | 4.5:1 | 7:1 | #595959 on white passes |
| Large text (18px+ bold) | 3:1 | 4.5:1 | #767676 on white passes |
| UI components (borders, icons) | 3:1 | — | Buttons, form borders, icons |
| Focus indicators | 3:1 | — | Must be visible against background |
Don't Rely on Color Alone
8% of men are color-blind. Never use color as the only way to convey information. Error states need icons or text, not just red borders. Chart data needs patterns or labels, not just different colors.
<input style="border-color: red;">
<div class="form-group error">
<label for="email">Email address</label>
<input id="email" aria-describedby="email-error"
aria-invalid="true">
<span id="email-error" class="error-msg">
⚠ Please enter a valid email address
</span>
</div>
Building Accessible Forms
Forms are where accessibility failures hit hardest — they're the gateway to conversions, signups, and transactions. An inaccessible form is a broken funnel.
<form>
<!-- Always use label + for/id pairing -->
<div class="field">
<label for="fullname">Full Name <span aria-hidden="true">*</span></label>
<input id="fullname" type="text" required
autocomplete="name"
aria-required="true">
</div>
<!-- Error messages linked via aria-describedby -->
<div class="field">
<label for="phone">Phone Number</label>
<input id="phone" type="tel"
autocomplete="tel"
aria-describedby="phone-hint"
pattern="[0-9]{10}">
<span id="phone-hint" class="hint">
10-digit number, no spaces or dashes
</span>
</div>
<!-- Group related fields with fieldset/legend -->
<fieldset>
<legend>Preferred contact method</legend>
<label>
<input type="radio" name="contact" value="email"> Email
</label>
<label>
<input type="radio" name="contact" value="phone"> Phone
</label>
</fieldset>
<button type="submit">Send Message</button>
</form>
Key form accessibility rules:
- Every input must have a visible
<label>(not just placeholder text) - Use
autocompleteattributes — they help password managers and assistive tech - Link error messages to inputs via
aria-describedby - Group related inputs with
<fieldset>and<legend> - Don't remove focus styles — style
:focus-visibleinstead
Testing Tools and Workflow
Automated tools catch about 30-40% of accessibility issues. The rest requires manual testing. Here's the workflow we use at Pillai Infotech:
Automated Testing (CI Pipeline)
| Tool | Type | Best For |
|---|---|---|
| axe-core | Library (Playwright/Cypress) | CI integration, most rules |
| Lighthouse | Chrome DevTools / CI | Quick audits, scoring |
| WAVE | Browser extension | Visual issue highlighting |
| eslint-plugin-jsx-a11y | ESLint plugin | Catching issues at write-time (React) |
| Pa11y | CLI / CI runner | Automated page-level testing |
Manual Testing Checklist
- Keyboard-only navigation — unplug your mouse, use only Tab/Enter/Escape/Arrow keys
- Screen reader testing — VoiceOver (Mac), NVDA (Windows, free), or JAWS
- Zoom to 200% — content must remain usable and not overlap
- Color contrast check — use the browser DevTools contrast checker
- Reduced motion — test with
prefers-reduced-motion: reduceenabled
/* Respect user's motion preferences */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* axe-core integration with Playwright */
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('homepage has no accessibility violations', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
Common Accessibility Mistakes We See
After auditing dozens of client sites, these are the issues we find most often:
- Missing alt text on images. Every
<img>needs analtattribute. Decorative images getalt=""(empty, not missing). Informative images get descriptive alt text. - Links that say "Click here" or "Read more." Screen readers often list all links on a page — "Click here" repeated 15 times is useless. Use descriptive link text: "Read our TypeScript best practices guide."
- No skip navigation link. Keyboard users shouldn't have to Tab through 50 nav links to reach content on every page.
- Auto-playing media. Video or audio that plays automatically disorients screen reader users and violates WCAG 1.4.2. Always require user interaction to play.
- Removing focus outlines without replacement.
outline: noneon everything makes your site unusable for keyboard navigators. Use:focus-visibleto show outlines only during keyboard navigation. - Form inputs without labels. Placeholder text disappears when typing and isn't reliably read by screen readers. Always use visible
<label>elements. - Inaccessible custom dropdowns. Custom select menus built with divs almost always break keyboard navigation and screen reader support. Use
<select>or a library like Headless UI that handles ARIA correctly.