# 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; } ``` ## Fix Patterns Catalog ### React / Next.js Fix Patterns #### Missing Alt Text (1.1.1) ```tsx // BEFORE // AFTER - Informational image Team collaborating around a whiteboard // AFTER - Decorative image ``` #### Non-Interactive Element with Click Handler (2.1.1) ```tsx // BEFORE
Click me
// AFTER - If it navigates Click me // AFTER - If it performs an action ``` #### Missing Focus Management in Modals (2.4.3) ```tsx // BEFORE function Modal({ isOpen, onClose, children }) { if (!isOpen) return null; return
{children}
; } // AFTER import { useEffect, useRef } from 'react'; function Modal({ isOpen, onClose, children, title }) { const modalRef = useRef(null); const previousFocus = useRef(null); useEffect(() => { if (isOpen) { previousFocus.current = document.activeElement; modalRef.current?.focus(); } else { previousFocus.current?.focus(); } }, [isOpen]); useEffect(() => { if (!isOpen) return; const handleKeydown = (e) => { if (e.key === 'Escape') onClose(); if (e.key === 'Tab') { const focusable = modalRef.current?.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); if (!focusable?.length) return; const first = focusable[0]; const last = focusable[focusable.length - 1]; if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); } else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); } } }; document.addEventListener('keydown', handleKeydown); return () => document.removeEventListener('keydown', handleKeydown); }, [isOpen, onClose]); if (!isOpen) return null; return ( ); } ``` #### Focus Appearance (2.4.11 -- NEW in WCAG 2.2) ```css /* BEFORE */ button:focus { outline: none; /* Removes default focus indicator */ } /* AFTER - Meets WCAG 2.2 Focus Appearance */ button:focus-visible { outline: 2px solid #005fcc; outline-offset: 2px; } ``` ```tsx // Tailwind CSS pattern ``` ### Vue Fix Patterns #### Missing Form Labels (1.3.1) ```vue ``` #### Dynamic Content Without Live Region (4.1.3) ```vue
{{ statusMessage }}

{{ statusMessage }}

``` #### Vue Router Navigation Announcements (2.4.2) ```typescript // router/index.ts router.afterEach((to) => { const title = to.meta.title || 'Page'; document.title = `${title} | My App`; // Announce route change to screen readers const announcer = document.getElementById('route-announcer'); if (announcer) { announcer.textContent = `Navigated to ${title}`; } }); ``` ```vue
``` ### Angular Fix Patterns #### Missing ARIA on Custom Components (4.1.2) ```typescript // BEFORE @Component({ selector: 'app-dropdown', template: `
{{ selected }}
{{ opt }}
` }) // AFTER @Component({ selector: 'app-dropdown', template: ` ` }) ``` #### Angular CDK A11y Module Integration ```typescript // Use Angular CDK for focus trap in dialogs import { A11yModule } from '@angular/cdk/a11y'; @Component({ template: `

Edit Profile

` }) ``` ### Svelte Fix Patterns #### Accessible Announcements (4.1.3) ```svelte {#if message}

{message}

{/if}
{#if message}

{message}

{/if}
``` #### SvelteKit Page Titles (2.4.2) ```svelte Dashboard | My App ``` ### Plain HTML Fix Patterns #### Skip Navigation Link (2.4.1) ```html
``` ```css .skip-link { position: absolute; top: -40px; left: 0; padding: 8px 16px; background: #005fcc; color: #fff; z-index: 1000; transition: top 0.2s; } .skip-link:focus { top: 0; } ``` #### Accessible Data Table (1.3.1) ```html
NameEmailRole
Alicealice@co.comAdmin
List of team members and their roles
Name Email Role
Alice alice@co.com Admin
```