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.

Quellinformationen

Repository
paulpas/agent-skill-router
Letzte Quellaktivität
23. September 2026 um 17:17
Erkannte Sprache von SKILL.md
Englisch
Sterne
6
Forks
0

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
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> );
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen