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.

Informações da origem

Repositório
paulpas/agent-skill-router
Última atividade na origem
23 de setembro de 2026 às 17:17
Idioma detectado do SKILL.md
inglês
Estrelas
6
Forks
0

Opções de instalação

Por padrão, está selecionado o prompt que primeiro revisa a origem. Você pode mudar para um comando direto ou baixar uma cópia local.

Revise os arquivos de origem

Leia o SKILL.md e os arquivos complementares exibidos pelo SkillsMP antes de decidir se vai instalar.

Exibindo SKILL.md

SKILL.md
Instruções da origem · Visualização somente leitura
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> );
Ver no GitHub
Este SKILL.md e muito grande, entao o SkillsMP mostra aqui apenas a primeira secao. Ver no GitHub