` with `onClick`
+- Adding `aria-*` attributes to any element
+- Implementing keyboard navigation or focus management
+- Receiving accessibility feedback from code review tools (CodeRabbit, ESLint a11y)
+- Building components that must support screen readers
+
+## Form Accessibility
+
+Missing `htmlFor` / `id` pairing and disconnected error messages are the most common issues flagged in code review.
+
+### Label Connection
+
+```tsx
+// BAD: label has no connection to input — screen readers cannot associate them
+Email
+
+
+// GOOD: htmlFor matches input id
+Email
+
+```
+
+### Required Fields
+
+```tsx
+// BAD: visual-only asterisk conveys nothing to screen readers
+Email *
+
+
+// GOOD: aria-required signals requirement programmatically
+
+ Email *
+
+
+```
+
+### Error Messages
+
+```tsx
+// BAD: error text exists visually but is not linked to the input
+
+Invalid email address
+
+// GOOD: aria-describedby connects input to its error message
+// aria-invalid signals the invalid state to screen readers
+
+{error && (
+
+ {error}
+
+)}
+```
+
+### Complete Accessible Form
+
+```tsx
+interface LoginFormProps {
+ onSubmit: (email: string, password: string) => void;
+}
+
+export function LoginForm({ onSubmit }: LoginFormProps) {
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [errors, setErrors] = useState<{ email?: string; password?: string }>({});
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ const newErrors: typeof errors = {};
+ if (!email) newErrors.email = 'Email is required';
+ if (!password) newErrors.password = 'Password is required';
+ if (Object.keys(newErrors).length) {
+ setErrors(newErrors);
+ return;
+ }
+ onSubmit(email, password);
+ };
+
+ return (
+
+ );
+}
+```
+
+## Semantic HTML
+
+Use the element that matches the intent. Screen readers and keyboard users depend on native semantics.
+
+```tsx
+// BAD: div has no role, no keyboard support, no accessible name
+Submit
+
+// GOOD: button is focusable, activates on Enter/Space, announces as "button"
+Submit
+```
+
+```tsx
+// BAD: non-semantic navigation
+ navigate('/home')}>Home
+
+// GOOD: anchor supports right-click, middle-click, and keyboard navigation
+Home
+```
+
+```tsx
+// BAD: heading hierarchy skipped (h1 to h4)
+Dashboard
+Recent Activity
+
+// GOOD: sequential heading levels
+Dashboard
+Recent Activity
+```
+
+## ARIA Attributes
+
+Use ARIA only when native HTML semantics are insufficient. Wrong ARIA is worse than no ARIA.
+
+### aria-label vs aria-labelledby
+
+```tsx
+// aria-label: inline string label — use when no visible label text exists
+
+
+
+
+// aria-labelledby: references another element's text — use when a visible label exists
+
+ Recent Orders
+ {/* content */}
+
+```
+
+### aria-describedby
+
+```tsx
+// Provides supplementary description beyond the label
+
+ Delete account
+
+This action cannot be undone.
+```
+
+### aria-live for Dynamic Content
+
+```tsx
+// Use aria-live to announce content that updates without a page reload
+// polite: waits for user to finish current action before announcing
+// assertive: interrupts immediately — use only for urgent errors
+
+export function StatusMessage({ message, isError }: { message: string; isError?: boolean }) {
+ return (
+
+ {message}
+
+ );
+}
+```
+
+### aria-expanded and aria-controls
+
+```tsx
+export function Accordion({ title, children }: { title: string; children: React.ReactNode }) {
+ const [isOpen, setIsOpen] = useState(false);
+ const contentId = useId();
+
+ return (
+
+
setIsOpen(prev => !prev)}>
+ {title}
+
+
+ {children}
+
+
+ );
+}
+```
+
+## Keyboard Navigation
+
+Every interactive element must be reachable and operable by keyboard alone.
+
+### Custom Dropdown
+
+```tsx
+export function Dropdown({ options, onSelect }: { options: string[]; onSelect: (value: string) => void }) {
+ const [isOpen, setIsOpen] = useState(false);
+ const [activeIndex, setActiveIndex] = useState(0);
+ const listId = useId();
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ switch (e.key) {
+ case 'ArrowDown':
+ e.preventDefault();
+ setActiveIndex(i => Math.min(i + 1, options.length - 1));
+ break;
+ case 'ArrowUp':
+ e.preventDefault();
+ setActiveIndex(i => Math.max(i - 1, 0));
+ break;
+ case 'Enter':
+ case ' ':
+ e.preventDefault();
+ if (isOpen) onSelect(options[activeIndex]);
+ setIsOpen(prev => !prev);
+ break;
+ case 'Escape':
+ setIsOpen(false);
+ break;
+ }
+ };
+
+ return (
+ setIsOpen(prev => !prev)}>
+
{options[activeIndex]}
+ {isOpen && (
+
+ {options.map((option, index) => (
+ {
+ onSelect(option);
+ setIsOpen(false);
+ }}
+ >
+ {option}
+
+ ))}
+
+ )}
+
+ );
+}
+```
+
+## Focus Management
+
+Focus must move logically when UI state changes — especially for modals and route transitions.
+
+### Modal Focus Trap
+
+```tsx
+export function Modal({ isOpen, onClose, title, children }: { isOpen: boolean; onClose: () => void; title: string; children: React.ReactNode }) {
+ const modalRef = useRef(null);
+ const previousFocusRef = useRef(null);
+
+ useEffect(() => {
+ if (isOpen) {
+ previousFocusRef.current = document.activeElement as HTMLElement;
+ modalRef.current?.focus();
+ } else {
+ previousFocusRef.current?.focus();
+ }
+ }, [isOpen]);
+
+ if (!isOpen) return null;
+
+ return (
+ e.key === 'Escape' && onClose()}>
+
{title}
+ {children}
+ Close
+
+ );
+}
+```
+
+## Images and Icons
+
+```tsx
+// BAD: decorative icon announced as unlabeled image
+
+
+// GOOD: decorative image hidden from screen readers
+
+
+// GOOD: meaningful image with descriptive alt text
+
+
+// GOOD: icon button with accessible label
+
+
+
+```
+
+## Reduced Motion
+
+Respect users who have requested reduced motion in their OS settings.
+
+```tsx
+export function useReducedMotion(): boolean {
+ const [prefersReduced, setPrefersReduced] = useState(false);
+
+ useEffect(() => {
+ const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
+ setPrefersReduced(mq.matches);
+ const handler = (e: MediaQueryListEvent) => setPrefersReduced(e.matches);
+ mq.addEventListener('change', handler);
+ return () => mq.removeEventListener('change', handler);
+ }, []);
+
+ return prefersReduced;
+}
+
+// Usage
+export function AnimatedCard({ children }: { children: React.ReactNode }) {
+ const reduceMotion = useReducedMotion();
+
+ return (
+
+ {children}
+
+ );
+}
+```
+
+## Anti-Patterns
+
+```tsx
+// BAD: onClick on non-interactive element with no keyboard support
+Click me
+
+// BAD: aria-label on a div that has no role
+...
+
+// BAD: placeholder used as a substitute for label
+
+
+// BAD: positive tabIndex creates unpredictable tab order
+Submit
+
+// BAD: aria-hidden on a focusable element — keyboard users get trapped
+Open
+
+// BAD: role="button" on div without keyboard handler
+Submit
+// Missing: tabIndex={0}, onKeyDown for Enter/Space
+```
+
+## Checklist
+
+Before submitting any interactive component for review:
+
+- [ ] Every ` `, ``, and `