Accessibility (a11y) ensures your app is usable by everyone, including people with disabilities who rely on screen readers, keyboard navigation, and other assistive technologies.
1. Semantic HTML — use the right elements:
1// BAD: div with click handler has no semantic meaning2<div onClick={handleClick}>Click me</div>34// GOOD: button is accessible by default5<button onClick={handleClick}>Click me</button>67// Use semantic elements for structure8<nav aria-label="Main navigation">9 <ul>10 <li><a href="/">Home</a></li>11 <li><a href="/about">About</a></li>12 </ul>13</nav>14<main>15 <article>16 <h1>Page Title</h1>17 <p>Content here</p>18 </article>19</main>20<aside>Sidebar content</aside>
2. ARIA attributes — enhance semantics:
1// Accessible icon button2<button aria-label="Close menu" onClick={close}>3 <CloseIcon />4</button>56// Live region for dynamic content7<div role="status" aria-live="polite">8 {isLoading && <p>Loading results...</p>}9 {error && <p role="alert">{error}</p>}10</div>1112// Form accessibility13<div>14 <label htmlFor="email">Email address</label>15 <input16 id="email"17 type="email"18 aria-describedby="email-help email-error"19 aria-invalid={hasError}20 aria-required={true}21 />22 <span id="email-help">We'll never share your email</span>23 {hasError && <span id="email-error" role="alert">Invalid email</span>}24</div>
3. Keyboard navigation:
1function Modal({ onClose, children }: { onClose: () => void; children: ReactNode }) {2 const modalRef = useRef<HTMLDivElement>(null);34 useEffect(() => {5 // Focus trap6 modalRef.current?.focus();78 const handleKeyDown = (e: KeyboardEvent) => {9 if (e.key === "Escape") onClose();1011 // Trap focus inside modal12 if (e.key === "Tab") {13 const focusable = modalRef.current?.querySelectorAll(14 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'15 );16 if (!focusable?.length) return;17 const first = focusable[0];18 const last = focusable[focusable.length - 1];19 if (e.shiftKey && document.activeElement === first) {20 e.preventDefault();21 (last as HTMLElement).focus();22 } else if (!e.shiftKey && document.activeElement === last) {23 e.preventDefault();24 (first as HTMLElement).focus();25 }26 }27 };2829 document.addEventListener("keydown", handleKeyDown);30 return () => document.removeEventListener("keydown", handleKeyDown);31 }, [onClose]);3233 return (34 <div35 ref={modalRef}36 role="dialog"37 aria-modal="true"38 aria-labelledby="modal-title"39 tabIndex={-1}40 >41 <h2 id="modal-title">Modal Title</h2>42 {children}43 <button onClick={onClose}>Close</button>44 </div>45 );46}
4. Focus management:
1function Dropdown() {2 const [isOpen, setIsOpen] = useState(false);3 const triggerRef = useRef<HTMLButtonElement>(null);4 const menuRef = useRef<HTMLDivElement>(null);56 const open = () => {7 setIsOpen(true);8 menuRef.current?.focus();9 };1011 const close = () => {12 setIsOpen(false);13 triggerRef.current?.focus(); // Return focus to trigger14 };1516 return (17 <div>18 <button19 ref={triggerRef}20 onClick={open}21 aria-expanded={isOpen}22 aria-haspopup="true"23 >24 Menu25 </button>26 {isOpen && (27 <div ref={menuRef} role="menu" tabIndex={-1}>28 <button role="menuitem" onClick={close}>Option 1</button>29 <button role="menuitem" onClick={close}>Option 2</button>30 </div>31 )}32 </div>33 );34}
5. Testing tools:
eslint-plugin-jsx-a11y — catches accessibility issues during development@axe-core/react — runtime accessibility checks in development mode