Web Accessibility
from zero to expert
Accessibility (shortened as a11y — 11 letters between a and y) means building websites and apps that everyone can use — including people with visual, auditory, motor, or cognitive disabilities.
Disabilities that affect web usage include:
- Visual — blindness, low vision, color blindness (use screen readers, magnifiers)
- Auditory — deafness, hard of hearing (need captions, transcripts)
- Motor — paralysis, tremors (can't use mouse, use keyboard or switch devices)
- Cognitive — dyslexia, ADHD, anxiety (need clear layout, no flashing content)
Web Content Accessibility Guidelines
WCAG (pronounced wuh-kag) is a set of guidelines published by the W3C (World Wide Web Consortium) that defines how to make web content accessible. Think of it as the rulebook.
- WCAG 2.0 — published 2008, became ISO standard
- WCAG 2.1 — published 2018, added mobile & cognitive improvements (current standard)
- WCAG 2.2 — published 2023, added more criteria especially for cognitive disabilities
- WCAG 3.0 — in progress, completely redesigned scoring model
The 4 POUR principles
Every WCAG guideline falls under one of these four core principles. Remember this acronym — interviewers love asking it.
Examples: alt text for images, captions for video, don't use color alone to convey meaning.
Examples: keyboard navigable, no seizure-inducing flashes, skip navigation links, enough time to read content.
Examples: readable text, predictable navigation, form error suggestions.
Examples: valid HTML, ARIA used correctly, no browser-specific hacks that break screen readers.
A, AA, AAA — what do they mean?
Semantic HTML — the foundation
Semantic HTML is using the right HTML element for the right job. Screen readers and other assistive technologies read the HTML structure to understand your page. If everything is a <div>, they have no idea what's a button, what's navigation, what's a heading.
Bad vs good — the most common mistake
/* ❌ BAD — a div pretending to be a button */ <div onclick="doSomething()" class="btn">Submit</div> // Problems: not focusable by keyboard, no role announced, // screen reader says "Submit" with no context /* ✅ GOOD */ <button type="button" onclick="doSomething()">Submit</button> // Free: keyboard focusable, Enter/Space activates it, // screen reader announces "Submit, button"
Key semantic elements to know
| Element | Purpose | Screen reader announces |
|---|---|---|
<header> | Page or section header | "banner" landmark |
<nav> | Navigation links | "navigation" landmark |
<main> | Primary page content | "main" landmark |
<footer> | Page or section footer | "contentinfo" landmark |
<aside> | Sidebar / related content | "complementary" landmark |
<button> | Clickable action | "[text], button" |
<a href> | Navigation link | "[text], link" |
<h1>–<h6> | Headings hierarchy | "[text], heading level N" |
<ul> / <ol> | Lists | "list, N items" |
<table> | Tabular data only | reads row/col headers |
Heading hierarchy — the most broken thing on enterprise apps
/* ❌ BAD — headings used for visual size, not structure */ <h1>Dashboard</h1> <h3>Revenue</h3> // jumped h2! <h5>Chart</h5> // jumped h4! /* ✅ GOOD — logical nested structure */ <h1>Dashboard</h1> <h2>Revenue</h2> <h3>Monthly Chart</h3> <h3>Yearly Chart</h3> <h2>Expenses</h2>
Keyboard navigation
Many users cannot use a mouse — they navigate entirely by keyboard. Your entire UI must be usable with just a keyboard.
Essential keyboard shortcuts to know
| Key | Action |
|---|---|
Tab | Move to next focusable element |
Shift + Tab | Move to previous focusable element |
Enter | Activate links and buttons |
Space | Activate buttons, checkboxes |
Arrow keys | Navigate within components (menus, radio groups, sliders) |
Escape | Close modals, dropdowns, tooltips |
What is naturally focusable?
These elements get Tab focus automatically — no extra work:
<a href>,<button>,<input>,<select>,<textarea>- Elements with
tabindex="0"
/* tabindex values */ tabindex="0" // add element to natural tab order tabindex="-1" // focusable via JS (.focus()) but NOT in tab order — used for modals tabindex="5" // ❌ AVOID — custom order is fragile and confusing
/* Skip link — first element in body */ <a href="#main-content" class="skip-link"> Skip to main content </a> /* CSS — visually hidden until focused */ .skip-link { position: absolute; top: -40px; left: 0; background: #000; color: #fff; padding: 8px; z-index: 100; } .skip-link:focus { top: 0; /* slides into view on focus */ }
Focus management
Focus indicator = the visible outline that shows which element is currently focused. Never remove it without replacing it with something better.
/* ❌ The most harmful CSS line in frontend history */ *:focus { outline: none; } /* ✅ Replace with custom visible indicator */ *:focus-visible { outline: 2px solid #4ade80; outline-offset: 3px; border-radius: 3px; } /* :focus-visible fires only on keyboard focus, not mouse click */
Modal / dialog focus trap
When a modal opens, focus must be trapped inside it. Users shouldn't be able to Tab out to content behind the modal.
// Focus trap pattern for modals function trapFocus(modalElement) { const focusable = modalElement.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); const first = focusable[0]; const last = focusable[focusable.length - 1]; modalElement.addEventListener('keydown', (e) => { if (e.key !== 'Tab') return; if (e.shiftKey) { if (document.activeElement === first) { last.focus(); e.preventDefault(); } } else { if (document.activeElement === last) { first.focus(); e.preventDefault(); } } }); first.focus(); // move focus into modal on open } // On close — return focus to the trigger element const trigger = document.querySelector('#open-modal-btn'); // when modal closes: trigger.focus();
Color & contrast ratios
WCAG defines minimum contrast ratios between text and its background. Contrast ratio is measured from 1:1 (same color) to 21:1 (black on white).
| Text type | AA minimum | AAA minimum |
|---|---|---|
| Normal text (under 18pt) | 4.5 : 1 | 7 : 1 |
| Large text (18pt+ or 14pt bold) | 3 : 1 | 4.5 : 1 |
| UI components & icons | 3 : 1 | — |
| Decorative / logo text | No requirement | — |
Tools for checking contrast
- WebAIM Contrast Checker — webaim.org/resources/contrastchecker
- Chrome DevTools — inspect any element, click the color swatch
- Figma — plugins like "Contrast" or "Stark"
- axe DevTools — browser extension that auto-flags contrast issues
Images & alt text
/* 3 types of images — different alt text strategy */ // 1. Informative image — describe what it shows <img src="chart.png" alt="Bar chart showing revenue grew 40% in Q4 2024" /> // 2. Decorative image — empty alt, screen reader skips it <img src="decorative-wave.svg" alt="" role="presentation" /> // 3. Functional image (inside a link/button) — describe the action <button> <img src="search-icon.svg" alt="Search" /> </button> // For icon-only buttons — use aria-label instead <button aria-label="Close dialog"> <svg aria-hidden="true">...</svg> </button>
Accessible forms
/* ❌ BAD — placeholder as label */ <input type="text" placeholder="Enter your email" /> // Placeholder disappears on typing — user forgets what field it was // Screen reader may or may not read placeholder as label /* ✅ GOOD — label associated with input */ <label for="email">Email address</label> <input type="email" id="email" name="email" aria-describedby="email-hint" aria-required="true" /> <span id="email-hint">We'll never share your email</span> /* Error state */ <input aria-invalid="true" aria-describedby="email-error" /> <span id="email-error" role="alert"> Please enter a valid email address </span>
ARIA — Accessible Rich Internet Applications
ARIA is a set of HTML attributes that modify the accessibility tree — what screen readers see. The golden rule:
<button> is always better than <div role="button">.
3 types of ARIA attributes
| Type | Attribute | Purpose | Example |
|---|---|---|---|
| Role | role | Overrides element type in accessibility tree | role="dialog", role="alert" |
| Property | aria-* | Adds meaning/relationship | aria-label, aria-required |
| State | aria-* | Dynamic current state | aria-expanded, aria-checked |
Most important ARIA attributes for interviews
/* aria-label — give a name when no visible text */ <button aria-label="Close dialog"> × </button> /* aria-labelledby — point to another element as the label */ <div role="dialog" aria-labelledby="dialog-title"> <h2 id="dialog-title">Confirm Delete</h2> </div> /* aria-describedby — point to description text */ <button aria-describedby="submit-hint">Submit</button> <span id="submit-hint">This will finalize your order</span> /* aria-expanded — for dropdowns, accordions */ <button aria-expanded="false" aria-controls="menu">Menu</button> <ul id="menu" hidden>...</ul> /* aria-live — announce dynamic content changes */ <div aria-live="polite"> // waits for user to finish action <div aria-live="assertive"> // interrupts immediately (for errors) /* aria-hidden — hide decorative elements from screen readers */ <span aria-hidden="true">→</span>
How screen readers work
Screen readers convert the accessibility tree (not the visual DOM) into speech or braille output. The most common ones:
| Screen reader | Platform | Notes |
|---|---|---|
| NVDA | Windows (free) | Most used for testing; pairs with Firefox/Chrome |
| JAWS | Windows (paid) | Most common in enterprise/banking environments |
| VoiceOver | macOS / iOS | Built-in; cmd+F5 to enable on Mac |
| TalkBack | Android | Built-in; important for React Native apps |
Dynamic content & live regions
When content changes without a page reload (SPA behavior, toast messages, loading states), screen readers don't know about it unless you tell them.
/* Toast / notification — use role="alert" for urgent messages */ <div role="alert"> Payment failed. Please check your card details. </div> // Automatically treated as aria-live="assertive" /* Status message — polite, non-urgent */ <div role="status"> Saved successfully </div> // Automatically treated as aria-live="polite" /* Loading state */ <button aria-busy="true" aria-label="Submitting form, please wait"> <span aria-hidden="true">Loading...</span> </button> /* Page title updates in SPA — critical for screen reader users */ // After route change, update document.title document.title = "Order Confirmation — MyApp";
Accessibility in React
React uses className and htmlFor instead of class and for, but all ARIA attributes work exactly the same.
// JSX — note htmlFor instead of for <label htmlFor="email">Email</label> <input id="email" type="email" /> // Accessible modal component with focus trap import { useEffect, useRef } from 'react'; function Modal({ isOpen, onClose, title, children }) { const modalRef = useRef(null); const triggerRef = useRef(document.activeElement); useEffect(() => { if (isOpen) { modalRef.current?.focus(); } else { triggerRef.current?.focus(); // return focus on close } }, [isOpen]); if (!isOpen) return null; return ( <div role="dialog" aria-modal="true" aria-labelledby="modal-title" ref={modalRef} tabIndex={-1} onKeyDown={(e) => e.key === 'Escape' && onClose()} > <h2 id="modal-title">{title}</h2> {children} <button onClick={onClose}>Close</button> </div> ); } // Route change announcement in React Router function RouteAnnouncer() { const location = useLocation(); useEffect(() => { document.title = getPageTitle(location.pathname); }, [location]); return null; }
eslint-plugin-jsx-a11y
Add this ESLint plugin to your project — it catches accessibility issues at code-writing time:
npm install --save-dev eslint-plugin-jsx-a11y // .eslintrc { "plugins": ["jsx-a11y"], "extends": ["plugin:jsx-a11y/recommended"] }
Pre-deployment accessibility checklist
- All images have meaningful alt text (or empty alt if decorative)
- All form inputs have associated labels (via for/id or aria-label)
- Color contrast ≥ 4.5:1 for normal text, ≥ 3:1 for large text
- Page is fully navigable by keyboard (Tab, Enter, Space, Escape)
- Focus indicator is visible — never outline: none without replacement
- Modal/dialog traps focus and returns it on close
- Dynamic content updates announced via aria-live or role="alert"
- Heading hierarchy is logical (h1 → h2 → h3, no skipping)
- Skip to main content link present at top of page
- No information conveyed by color alone
- Interactive elements use correct semantic HTML (button, not div)
- Icon-only buttons have aria-label
- Videos have captions, audio has transcripts
- Run axe DevTools — fix all critical + serious issues
- Test with keyboard-only — complete one full user journey
Interview Q&A — JPMC style
Q: What is WCAG and what level do you target?
Say: "WCAG stands for Web Content Accessibility Guidelines, published by the W3C. It's organized around 4 POUR principles — Perceivable, Operable, Understandable, Robust. I target WCAG 2.1 AA as the baseline for all production work — that's also the legal requirement under ADA and EAA. For critical flows like payments or error states, I consider AAA criteria like enhanced contrast."
Q: How do you handle accessibility in a React component library?
Say: "I make sure semantic HTML is the foundation — buttons are always button elements, links are always anchor tags. I use eslint-plugin-jsx-a11y to catch issues early. For dynamic components like modals, I implement focus management — trap focus inside the modal, return it to the trigger on close. For form components, I always associate labels with inputs via htmlFor/id. And I test with keyboard navigation and VoiceOver before shipping."
Q: How do you make a custom dropdown accessible?
Say: "A custom dropdown needs: the trigger button with aria-expanded reflecting open/closed state, aria-haspopup="listbox", and aria-controls pointing to the list. The list gets role="listbox", each item gets role="option" with aria-selected. Arrow keys move between options, Enter/Space select, Escape closes it. This follows the ARIA Authoring Practices Guide combobox pattern."
Q: What tools do you use for accessibility testing?
Say: "Three layers: automated, manual, and assistive tech testing. For automated I use axe-core (via axe DevTools browser extension or jest-axe in unit tests) — catches about 30-40% of issues. For manual I do keyboard-only walkthroughs and zoom to 200%. For assistive tech I test with VoiceOver on Mac. No tool catches everything — keyboard testing in particular surfaces issues automated tools miss."
Q: Tell me about a time you improved accessibility in a real project.
Your angle with Superset: "In my Superset work, I customized chart components for business needs. From an accessibility perspective, charts are one of the hardest areas — they're inherently visual. I made sure each chart had a proper aria-label describing the data it showed, added a text summary table as an accessible alternative, and ensured the color palettes we used had sufficient contrast ratios. I also made the interactive filtering controls keyboard-accessible."