Skip to main content

keyboard-navigation-focus-management

Implements keyboard accessibility and focus management: focus APIs, roving tabindex, focus trapping in modals, focus restoration on close, visible focus indicators, keyboard event handlers for Enter/Space/Escape/Arrow keys, and skip links.

Informations de source

Dépôt
paulpas/agent-skill-router
Dernière activité de la source
23 septembre 2026 à 17:17
Langue détectée de SKILL.md
anglais
Étoiles
6
Forks
0

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
keyboard-navigation-focus-management
description
Implements keyboard accessibility and focus management: focus APIs, roving tabindex, focus trapping in modals, focus restoration on close, visible focus indicators, keyboard event handlers for Enter/Space/Escape/Arrow keys, and skip links.
license
MIT
compatibility
opencode
metadata
{"version":"1.0.0","domain":"coding","role":"implementation","scope":"implementation","output-format":"code","content-types":["code","patterns","examples"],"triggers":"keyboard navigation, focus management, roving tabindex, focus trap, visible focus, tabindex, Escape key, keyboard events","related-skills":"semantic-html-aria-accessibility-tree, react-accessibility-components-patterns","archetypes":["tactical","implementation"],"anti_triggers":["mouse-only design","brainstorming","styling-only"],"response_profile":{"verbosity":"low","directive_strength":"high","abstraction_level":"operational"}}
# Keyboard Navigation & Focus Management Implements keyboard accessibility and focus management for interactive components: focus APIs (focus(), setFocusVisible), focus-visible pseudo-class, tabindex patterns (0/-1), roving tabindex for lists, focus trapping in modals, focus restoration on close, keyboard event handlers for Enter/Space/Escape/Arrow keys, and skip links. Load when implementing interactive components, fixing keyboard navigation issues, or managing focus in complex UIs. ## TL;DR Checklist - [ ] Make all interactive elements reachable via Tab key - [ ] Implement visible `:focus-visible` indicator (never `outline: none`) - [ ] Use `tabindex="0"` to include in natural tab order - [ ] Use `tabindex="-1"` for elements focusable by script only - [ ] Implement roving tabindex for lists/grids (only one in tab order) - [ ] Trap focus in modals (Tab at end jumps to first) - [ ] Restore focus when modal closes - [ ] Handle Escape key to close modals/dropdowns - [ ] Implement skip links for keyboard users - [ ] Test with Tab, Shift+Tab, Arrow keys, Enter, Space, Escape --- ## When to Use Use this skill when: - Building interactive components (menu, dialog, tabs, listbox, combobox) - Fixing keyboard navigation issues in existing components - Implementing focus management in modals or complex UIs - Adding keyboard shortcut handlers - Ensuring Tab key navigates through page logically - Testing keyboard-only navigation --- ## When NOT to Use Avoid this skill for: - Basic HTML forms (native behavior sufficient) - Styling focus indicators (that's CSS-focused) - Framework-specific patterns (use React/Vue-specific skills) - Screen reader testing (different skill) --- ## Keyboard Navigation Foundation ### Understanding Focus **Focus** is the current interactive element that receives keyboard input. Only one element can have focus at a time. **Focusable elements by default:** - `<button>`, `<a href>`, `<input>`, `<textarea>`, `<select>` - Elements with `tabindex="0"` or positive tabindex - Elements with `tabindex="-1"` (focusable by script only) **Not focusable by default:** - `<div>`, `<span>`, `<p>`, `<h1>` (etc.) - Disabled form elements ### Tab Order Default tab order follows DOM order. Can be influenced by `tabindex`: ```html <!-- DOM Order (natural tab order) --> <button>1</button> <!-- Tab 1st --> <input> <!-- Tab 2nd --> <button> <!-- Tab 3rd --> <!-- With tabindex (avoid if possible!) --> <button tabindex="2"> <!-- Tab 2nd --> <input tabindex="1"> <!-- Tab 1st --> <button tabindex="3"> <!-- Tab 3rd --> <!-- ⚠️ AVOID tabindex > 0 — causes confusion and maintenance burden --> ``` --- ## Implementation Patterns ### Pattern 1: Focus Visible Indicator ```css /* ✅ GOOD: Visible focus indicator using :focus-visible */ button:focus-visible { outline: 3px solid #2563eb; outline-offset: 2px; } input:focus-visible { border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); } a:focus-visible { outline: 2px dashed #2563eb; } /* High contrast mode support */ @media (prefers-contrast: more) { button:focus-visible { outline-width: 4px; } } /* ❌ BAD: Removing focus indicator without replacement --> button:focus { outline: none; /* Keyboard users can't see focus! */ } /* Remove default outline if replacing with custom */ button { outline: none; } button:focus-visible { /* Custom indicator here */ border: 2px solid #2563eb; } ``` **Key pattern:** Use `:focus-visible` (shows for keyboard) not `:focus` (shows for all). ### Pattern 2: Making Elements Focusable ```html <!-- Focusable by default (no tabindex needed) --> <button>Click me</button> <a href="/">Link</a> <input type="text"> <!-- Make div focusable by script (tabindex="-1") --> <div tabindex="-1" id="message"> Dynamic message (can be focused programmatically) </div> <!-- Include in tab order (tabindex="0") --> <div tabindex="0" role="button" @click="handleClick"> Custom button (now in tab order) </div> <!-- ❌ AVOID: Positive tabindex (causes confusion) --> <div tabindex="1">Too early</div> <div tabindex="2">Too late</div> <!-- ✅ BETTER: Fix DOM order instead --> <div>Correct order</div> <div>Without tabindex needed</div> ``` ### Pattern 3: Roving Tabindex for Lists/Grids Roving tabindex pattern: Only one item in tab order, Arrow keys move focus within list. ```typescript // Roving tabindex hook export function useRovingTabindex(items: string[]) { const [activeIndex, setActiveIndex] = useState(0); const itemRefs = useRef<HTMLElement[]>([]); useEffect(() => { itemRefs.current[activeIndex]?.focus(); }, [activeIndex]); const handleKeyDown = (e: React.KeyboardEvent, index: number) => { switch (e.key) { case 'ArrowDown': case 'ArrowRight': e.preventDefault(); setActiveIndex((prev) => (prev + 1) % items.length); break; case 'ArrowUp': case 'ArrowLeft': e.preventDefault(); setActiveIndex((prev) => (prev - 1 + items.length) % items.length); break; case 'Home': e.preventDefault(); setActiveIndex(0); break; case 'End': e.preventDefault(); setActiveIndex(items.length - 1); break; } }; return { activeIndex, itemRefs, handleKeyDown, }; } // Usage in list component export function AccessibleList({ items }: { items: string[] }) { const { activeIndex, itemRefs, handleKeyDown } = useRovingTabindex(items); return ( <ul role="listbox"> {items.map((item, index) => ( <li key={index}> <button ref={(el) => { if (el) itemRefs.current[index] = el; }} role="option" aria-selected={index === activeIndex} tabIndex={index === activeIndex ? 0 : -1} onKeyDown={(e) => handleKeyDown(e, index)} > {item} </button> </li> ))} </ul> ); } ``` **Key pattern:** - Only active item has `tabindex="0"` (in tab order) - Other items have `tabindex="-1"` (focusable by script) - Arrow keys change which item is active - Active item automatically focused ### Pattern 4: Focus Trap in Modal ```typescript export function useFocusTrap(isActive: boolean = true) { const containerRef = useRef<HTMLDivElement>(null); const firstElementRef = useRef<HTMLElement | null>(null); const lastElementRef = useRef<HTMLElement | null>(null); useEffect(() => { if (!isActive || !containerRef.current) return; // Get all focusable elements const focusableSelector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'; const focusable = Array.from( containerRef.current.querySelectorAll(focusableSelector) ) as HTMLElement[]; firstElementRef.current = focusable[0]; lastElementRef.current = focusable[focusable.length - 1]; // Focus first element on mount firstElementRef.current?.focus(); // Handle Tab/Shift+Tab at boundaries function handleKeyDown(e: KeyboardEvent) { if (e.key !== 'Tab') return; if (e.shiftKey) { // Shift+Tab at first element → jump to last if (document.activeElement === firstElementRef.current) { e.preventDefault(); lastElementRef.current?.focus(); } } else { // Tab at last element → jump to first if (document.activeElement === lastElementRef.current) { e.preventDefault(); firstElementRef.current?.focus(); } } } containerRef.current.addEventListener('keydown', handleKeyDown); return () => { containerRef.current?.removeEventListener('keydown', handleKeyDown); }; }, [isActive]); return containerRef; } // Usage in modal/dialog export function Modal({ isOpen, onClose }) { const modalRef = useFocusTrap(isOpen); return ( isOpen && ( <div ref={modalRef} role="dialog" aria-modal="true"> <h2>Dialog Title</h2> <p>Dialog content</p> <button onClick={onClose}>Close</button> <button>Action 1</button> <button>Action 2</button> </div> ) ); } ``` ### Pattern 5: Focus Restoration on Close ```typescript export function useRestoreFocus() { const previousFocusRef = useRef<HTMLElement | null>(null); const saveFocus = () => { previousFocusRef.current = document.activeElement as HTMLElement; }; const restoreFocus = () => { setTimeout(() => { previousFocusRef.current?.focus(); }, 0); }; return { saveFocus, restoreFocus }; } // Usage export function Modal({ isOpen, onClose }) { const { saveFocus, restoreFocus } = useRestoreFocus(); const handleOpen = () => { saveFocus(); // Open modal }; const handleClose = () => { // Close modal onClose(); // Restore focus to trigger button restoreFocus(); }; return ( <> <button onClick={handleOpen}>Open Modal</button> {isOpen && ( <div role="dialog"> <h2>Dialog</h2> <button onClick={handleClose}>Close</button> </div> )} </> ); } ``` ### Pattern 6: Keyboard Event Handlers ```typescript // Generic keyboard handler export function useKeyboardNavigation(callbacks: { onEnter?: () => void; onEscape?: () => void; onSpace?: () => void; onArrowUp?: () => void; onArrowDown?: () => void; onArrowLeft?: () => void; onArrowRight?: () => void; onHome?: () => void; onEnd?: () => void; }) { const handleKeyDown = (e: React.KeyboardEvent) => { switch (e.key) { case 'Enter': callbacks.onEnter?.(); break; case ' ': callbacks.onSpace?.(); break; case 'Escape': callbacks.onEscape?.(); break; case 'ArrowUp': callbacks.onArrowUp?.(); break; case 'ArrowDown': callbacks.onArrowDown?.(); break; case 'ArrowLeft': callbacks.onArrowLeft?.(); break; case 'ArrowRight': callbacks.onArrowRight?.(); break; case 'Home': callbacks.onHome?.(); break; case 'End': callbacks.onEnd?.(); break; } }; return handleKeyDown; } // Usage in menu component export function Menu() { const [activeIndex, setActiveIndex] = useState(0); const handleKeyDown = useKeyboardNavigation({ onEnter: () => console.log('Select'), onEscape: () => console.log('Close'), onArrowDown: () => setActiveIndex((i) => i + 1), onArrowUp: () => setActiveIndex((i) => i - 1), }); return ( <ul onKeyDown={handleKeyDown} role="menu"> <li role="menuitem">Option 1</li> <li role="menuitem">Option 2</li> <li role="menuitem">Option 3</li> </ul> );
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub