# Framework-Specific Accessibility Patterns ## React / Next.js ### Common Issues and Fixes **Image alt text:** ```jsx // ❌ Bad // ✅ Good Team collaborating in office Team collaborating in office // ✅ Decorative image ``` **Form labels:** ```jsx // ❌ Bad — placeholder as label // ✅ Good — explicit label // ✅ Good — aria-label for icon-only inputs ``` **Click handlers on divs:** ```jsx // ❌ Bad — not keyboard accessible
Click me
// ✅ Good — use button // ✅ If div is required — add keyboard support
{ if (e.key === 'Enter' || e.key === ' ') handleClick(); }} > Click me
``` **SPA route announcements (Next.js App Router):** ```jsx // Layout component — announce page changes 'use client'; import { usePathname } from 'next/navigation'; import { useEffect, useState } from 'react'; export function RouteAnnouncer() { const pathname = usePathname(); const [announcement, setAnnouncement] = useState(''); useEffect(() => { const title = document.title; setAnnouncement(`Navigated to ${title}`); }, [pathname]); return (
{announcement}
); } ``` **Focus management after dynamic content:** ```jsx // After adding item to list, announce it const [items, setItems] = useState([]); const statusRef = useRef(null); const addItem = (item) => { setItems([...items, item]); // Announce to screen readers statusRef.current.textContent = `${item.name} added to list`; }; return ( <>
{/* list content */} ); ``` ### React-Specific Libraries - `@radix-ui/*` — accessible primitives (Dialog, Tabs, Select, etc.) - `@headlessui/react` — unstyled accessible components - `react-aria` — Adobe's accessibility hooks - `eslint-plugin-jsx-a11y` — lint rules for JSX accessibility ## Vue 3 ### Common Issues and Fixes **Dynamic content announcements:** ```vue ``` **Conditional rendering with focus:** ```vue ``` ### Vue-Specific Libraries - `vue-announcer` — route change announcements - `@headlessui/vue` — accessible components - `eslint-plugin-vuejs-accessibility` — lint rules ## Angular ### Common Issues and Fixes **CDK accessibility utilities:** ```typescript import { LiveAnnouncer } from '@angular/cdk/a11y'; import { FocusTrapFactory } from '@angular/cdk/a11y'; @Component({...}) export class MyComponent { constructor( private liveAnnouncer: LiveAnnouncer, private focusTrapFactory: FocusTrapFactory ) {} addItem(item: Item) { this.items.push(item); this.liveAnnouncer.announce(`${item.name} added`); } openDialog(element: HTMLElement) { const focusTrap = this.focusTrapFactory.create(element); focusTrap.focusInitialElement(); } } ``` **Template-driven forms:** ```html ``` ### Angular-Specific Tools - `@angular/cdk/a11y` — `FocusTrap`, `LiveAnnouncer`, `FocusMonitor` - `codelyzer` — a11y lint rules for Angular templates ## Svelte / SvelteKit ### Common Issues and Fixes ```svelte
Action
{#if isOpen}
Panel content
{/if} ``` **Note:** Svelte has built-in a11y warnings in the compiler — it flags missing alt text, click-without-keyboard, and other common issues at build time. ## Plain HTML ### Checklist for Static Sites ```html Descriptive Page Title

Page Heading

``` ## CSS Accessibility Patterns ### Focus Indicators ```css /* ❌ Bad — removes focus indicator entirely */ :focus { outline: none; } /* ✅ Good — custom focus indicator */ :focus-visible { outline: 2px solid #005fcc; outline-offset: 2px; } /* ✅ Good — enhanced for high contrast mode */ @media (forced-colors: active) { :focus-visible { outline: 2px solid ButtonText; } } ``` ### Reduced Motion ```css /* ✅ Respect prefers-reduced-motion */ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } } ``` ### Screen Reader Only ```css .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border-width: 0; } ```