| name | fixing-accessibility |
| description | Fix accessibility issues hands-on — focus management, keyboard traps, missing ARIA labels, skip links, color contrast failures, interactive element sizing, and screen reader announcements. Use when asked to "fix accessibility issues", "keyboard navigation broken", "focus trap", "ARIA label missing", "screen reader not announcing", "skip to content", "tab order wrong", "focus ring missing", "color contrast fails", or "make this accessible". Do NOT use for: full WCAG audit from scratch — see accessibility-audit. Do NOT use for: motion/animation accessibility — see fixing-motion-performance.
|
| origin | adapted:MIT © Ibelick |
| license | MIT © 2026 Vũ Văn Tâm |
| version | 1.0.0 |
| compatibility | React ≥ 18, HTML5. Patterns apply to any framework. |
When to Use
- Use when: a component fails keyboard navigation (can't tab to it, gets stuck)
- Use when: screen reader reads nothing or reads wrong text
- Use when: focus ring is hidden (
:focus { outline: none } exists)
- Use when: modal/dialog opens but focus doesn't move into it
- Use when: color contrast ratio is below 4.5:1 (AA) or 3:1 (AA large)
- Do NOT use for: initial WCAG audit — use accessibility-audit for that
- Do NOT use for: performance of animations — use fixing-motion-performance
Focus Management
* { outline: none; }
:focus { outline: none; }
:focus-visible {
outline: 2px solid var(--color-focus);
outline-offset: 2px;
border-radius: 2px;
}
:focus:not(:focus-visible) { outline: none; }
import { useEffect, useRef } from 'react';
function Modal({ open, onClose, children }) {
const firstFocusRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (open) firstFocusRef.current?.focus();
}, [open]);
return open ? (
<div role="dialog" aria-modal="true" aria-label="Dialog">
<button ref={firstFocusRef} onClick={onClose}>Close</button>
{children}
</div>
) : null;
}
Focus Trap (Modal / Drawer)
function useFocusTrap(ref: RefObject<HTMLElement>, active: boolean) {
useEffect(() => {
if (!active || !ref.current) return;
const focusable = ref.current.querySelectorAll<HTMLElement>(
'a[href], button:not([disabled]), input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
function handleKeyDown(e: KeyboardEvent) {
if (e.key !== 'Tab') return;
if (e.shiftKey) {
if (document.activeElement === first) { e.preventDefault(); last.focus(); }
} else {
if (document.activeElement === last) { e.preventDefault(); first.focus(); }
}
}
document.addEventListener('keydown', handleKeyDown);
return .(, handleKeyDown);
}, [active]);
}
ARIA Labels
<button onClick={onClose}>
<XIcon />
</button>
<button onClick={onClose} aria-label="Close dialog">
<XIcon aria-hidden="true" /> {/* hide decorative icon from screen readers */}
</button>
<input type="text" placeholder="Search..." />
<label htmlFor="search">Search</label>
<input id="search" type="text" />
<input type="text" aria-label="Search products" />
Live Regions — Dynamic Announcements
function StatusAnnouncer({ message }: { message: string }) {
return (
<div
role="status" /* polite — waits for user to finish */
aria-live="polite"
aria-atomic="true"
className="sr-only" /* visually hidden, readable by AT */
>
{message}
</div>
);
}
<div role="alert" aria-live="assertive">{error}</div>
Skip Links
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4 focus:z-50 focus:px-4 focus:py-2 focus:bg-white focus:text-black focus:rounded"
>
Skip to main content
</a>
<main id="main-content" tabIndex={-1}>
{/* tabIndex={-1} allows programmatic focus without adding to tab order */}
</main>
Touch Target Sizing
button, a, [role="button"] {
min-width: 44px;
min-height: 44px;
}
.icon-btn {
padding: 10px;
}
Common Fixes at a Glance
| Issue | Fix |
|---|
| No skip link | Add <a href="#main">Skip to content</a> as first element |
| Focus outline removed | Replace outline: none with :focus-visible approach |
| Icon button unlabeled | Add aria-label + aria-hidden on icon |
| Modal doesn't trap focus | Implement useFocusTrap or use Radix UI Dialog |
| Dynamic content silent | Wrap in role="status" / aria-live |
| Tab order wrong | Fix DOM order; avoid tabindex > 0 |
| Form field has no label | Add <label htmlFor> or aria-label |
| Touch target < 44px | Add padding or min-width/height: 44px |
Anti-Fake-Pass Rules
Before claiming accessibility fixes are done, you MUST show:
Reference: gates/anti-fake-pass-gate.md