# Framework-Specific Accessibility Patterns
## React / Next.js
### Common Issues and Fixes
**Image alt text:**
```jsx
// ❌ Bad
// ✅ Good
// ✅ 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
```
### 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 TitleSkip to main content
// 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 (
e.stopPropagation()}
>
{children}
);
}
```
#### 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: `
{{ opt }}
`
})
```
#### Angular CDK A11y Module Integration
```typescript
// Use Angular CDK for focus trap in dialogs
import { A11yModule } from '@angular/cdk/a11y';
@Component({
template: `