From harness-claude
Guides keyboard navigation implementation for web apps, ensuring interactive elements like custom widgets, modals, and tabs are operable without a mouse per WCAG 2.1.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Ensure all interactive elements are reachable and operable via keyboard alone without requiring a mouse
Tests keyboard navigation in frontend apps with React, Vue, CSS for accessibility. Provides step-by-step guidance, generates code/configs, validates outputs when writing tests.
Implements WCAG 2.1/2.2 compliance, ARIA patterns, keyboard navigation, focus management, and accessibility testing for web components.
Implements a11y best practices like semantic HTML, keyboard navigation, ARIA attributes, landmarks, focus management, and WCAG 2.1 AA compliance for UI building and audits.
Share bugs, ideas, or general feedback.
Ensure all interactive elements are reachable and operable via keyboard alone without requiring a mouse
Use native interactive elements. <button>, <a>, <input>, <select>, and <textarea> are keyboard-accessible by default. They receive focus, respond to Enter/Space, and are announced by screen readers. Never recreate this behavior on <div> or <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.
<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;
}
tabindex values greater than 0 — they disrupt the natural flow. Use only tabindex="0" (add to tab order) and tabindex="-1" (programmatically focusable but not in tab order).// 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}>
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;
}
}
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
}
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();
}
});
}
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.
tabindex values:
0: Element is added to the natural tab order based on DOM position-1: Element is focusable via element.focus() but not via Tab key1, 2, etc.): Avoid — they override natural tab order and create maintenance nightmaresRoving 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.
https://www.w3.org/WAI/WCAG21/Understanding/keyboard