git clone https://github.com/Intense-Visions/harness-engineering
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/codex/a11y-keyboard-navigation" ~/.claude/skills/intense-visions-harness-engineering-a11y-keyboard-navigation-be10a6 && rm -rf "$T"
agents/skills/codex/a11y-keyboard-navigation/SKILL.mdKeyboard Navigation
Ensure all interactive elements are reachable and operable via keyboard alone without requiring a mouse
When to Use
- Building any interactive web component
- Adding custom widgets (dropdowns, sliders, tabs, drag-and-drop)
- Reviewing focus order after layout changes
- Implementing focus management for modals, drawers, or dynamic content
- Ensuring compliance with WCAG 2.1 Success Criterion 2.1.1 (Keyboard)
Instructions
-
Use native interactive elements.
,<button>
,<a>
,<input>
, and<select>
are keyboard-accessible by default. They receive focus, respond to Enter/Space, and are announced by screen readers. Never recreate this behavior on<textarea>
or<div>
.<span> -
Provide a visible focus indicator. Users must see where focus is. Never remove the outline without providing an alternative.
/* Remove default only if providing a custom indicator */ :focus-visible { outline: 2px solid #005fcc; outline-offset: 2px; } /* Never do this without a replacement */ /* :focus { outline: none; } */
Use
:focus-visible instead of :focus so the indicator appears only for keyboard users, not mouse clicks.
- Add a skip navigation link as the first focusable element on every page. This lets keyboard users bypass repetitive navigation.
<body> <a href="#main-content" class="skip-link">Skip to main content</a> <nav><!-- navigation --></nav> <main id="main-content"><!-- content --></main> </body>
.skip-link { position: absolute; left: -10000px; } .skip-link:focus { position: static; display: block; }
- Maintain a logical tab order. The DOM order should match the visual order. Avoid
values greater than 0 — they disrupt the natural flow. Use onlytabindex
(add to tab order) andtabindex="0"
(programmatically focusable but not in tab order).tabindex="-1"
// tabindex="0" — makes a non-interactive element focusable <div role="listbox" tabIndex={0}> // tabindex="-1" — focusable via JavaScript, not via Tab <div id="error-message" tabIndex={-1} ref={errorRef}>
- Implement keyboard event handlers for custom widgets. Follow WAI-ARIA Authoring Practices for expected key bindings:
- Tabs: Arrow keys move between tabs, Tab moves to the tab panel
- Menus: Arrow keys navigate items, Enter selects, Escape closes
- Combobox: Arrow keys navigate options, Enter selects, Escape clears
- Dialog: Tab cycles within the dialog, Escape closes
function handleKeyDown(e: React.KeyboardEvent) { switch (e.key) { case 'ArrowDown': e.preventDefault(); focusNextItem(); break; case 'ArrowUp': e.preventDefault(); focusPreviousItem(); break; case 'Home': e.preventDefault(); focusFirstItem(); break; case 'End': e.preventDefault(); focusLastItem(); break; case 'Escape': closeMenu(); break; } }
- Manage focus when content changes dynamically. When a modal opens, move focus into it. When it closes, return focus to the trigger element. When a route changes in a SPA, move focus to the new page heading or main content.
function openModal() { triggerRef.current = document.activeElement as HTMLElement; setIsOpen(true); // Focus the modal after render requestAnimationFrame(() => modalRef.current?.focus()); } function closeModal() { setIsOpen(false); triggerRef.current?.focus(); // return focus to trigger }
- Implement focus trapping in modals and dialogs. When a modal is open, Tab and Shift+Tab should cycle only through focusable elements within the modal — not escape to the page behind.
function trapFocus(container: HTMLElement) { const focusable = container.querySelectorAll<HTMLElement>( 'a[href], button:not([disabled]), input:not([disabled]), select, textarea, [tabindex]:not([tabindex="-1"])' ); const first = focusable[0]; const last = focusable[focusable.length - 1]; container.addEventListener('keydown', (e) => { if (e.key !== 'Tab') return; if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); } else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); } }); }
- Never create keyboard traps. Users must always be able to navigate away from any component using standard keys (Tab, Escape). The only exception is modal dialogs, which trap focus intentionally but provide an Escape key exit.
Details
WCAG requirements: SC 2.1.1 (Keyboard) requires all functionality to be operable via keyboard. SC 2.1.2 (No Keyboard Trap) requires users to be able to move focus away from any component. SC 2.4.7 (Focus Visible) requires a visible focus indicator.
values:tabindex
- Omitted or not applicable: Element follows default focusability (interactive elements are focusable, non-interactive are not)
: Element is added to the natural tab order based on DOM position0
: Element is focusable via-1
but not via Tab keyelement.focus()- Positive values (
,1
, etc.): Avoid — they override natural tab order and create maintenance nightmares2
Roving tabindex pattern: For composite widgets (tab lists, toolbars), only one item has
tabindex="0" at a time. Arrow keys move tabindex="0" to the next item and tabindex="-1" to the previous. Tab moves focus out of the widget entirely.
Testing keyboard navigation: Unplug your mouse and use the application with keyboard only. Tab through every page, activate every button, fill every form, dismiss every dialog. If you get stuck or cannot see where focus is, there is a bug.
Source
https://www.w3.org/WAI/WCAG21/Understanding/keyboard
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.