Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsBlogPro
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Authors
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges
  • Chrome Extension
  • Skill Manager

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

ProTermsPrivacyRefunds
Back to skills

Keyboard Navigation Focus Management

ASecurity

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.

6 stars
0 votes
0 copies
0 views
Added 9/23/2026
developmenttypescriptgoreactvuetestingapi

Works with

cliapi

Security Analysis

A100/100

Scanned 9/23/2026

$npx -y skills add paulpas/agent-skill-router --skill keyboard-navigation-focus-management --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Keyboard Navigation Focus Management?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for Keyboard Navigation Focus Management
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/paulpas-keyboard-navigation-focus-management/badge)](https://www.skillsdirectory.com/skills/paulpas-keyboard-navigation-focus-management)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
Files
SKILL.md
---
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>
  );
}
```

### Pattern 7: Skip Links

```html
<!-- Skip link (appears on Tab, hidden visually) -->
<a href="#main" class="skip-link">Skip to main content</a>

<header>
  <nav>
    <!-- Navigation with many links -->
  </nav>
</header>

<main id="main">
  <!-- Main content -->
</main>

<style>
  .skip-link {
    position: absolute;
    top: -40px;
    left: 0;
    background: #000;
    color: #fff;
    padding: 8px;
    text-decoration: none;
    z-index: 100;
  }

  .skip-link:focus {
    top: 0;
  }
</style>
```

---

## Keyboard Event Reference

### Standard Keys for Web Components

| Key | Element | Action |
|-----|---------|--------|
| **Enter** | Button, Link | Activate |
| **Space** | Button, Checkbox | Toggle/Activate |
| **Escape** | Modal, Menu, Popup | Close |
| **Tab** | Any | Move focus to next |
| **Shift+Tab** | Any | Move focus to previous |
| **↑ ↓ ← →** | Menu, List, Tree, Tabs | Navigate |
| **Home** | Menu, List | Jump to first |
| **End** | Menu, List | Jump to last |

### Preventing Default Behavior

```typescript
function handleKeyDown(e: React.KeyboardEvent) {
  if (e.key === 'Enter') {
    e.preventDefault(); // Prevent form submission
    // Custom behavior here
  }
}
```

---

## Focus Management Checklist

**Implementation:**
- [ ] All interactive elements keyboard accessible
- [ ] Tab order logical (matches visual layout)
- [ ] Focus indicator visible (3:1 contrast minimum)
- [ ] No keyboard traps (Tab can always escape)
- [ ] Modals trap focus (Tab loops within modal)
- [ ] Focus restored when modal closes
- [ ] Arrow keys work in lists/menus (roving tabindex)
- [ ] Escape closes modals/dropdowns
- [ ] Skip link bypasses navigation

**Testing:**
- [ ] Tab through entire page
- [ ] Verify tab order matches visual order
- [ ] Test focus trap in modal (Tab doesn't escape)
- [ ] Verify focus restoration on close
- [ ] Test keyboard shortcuts (Enter, Space, Escape, Arrows)
- [ ] Test with keyboard only (no mouse)
- [ ] Test with screen reader (focus announcements)

---

## Constraints

### MUST DO

- Make all interactive elements keyboard accessible (Tab key)
- Implement visible `:focus-visible` indicator (never just `outline: none`)
- Use `tabindex="0"` for elements that need to be in tab order
- Use `tabindex="-1"` for elements focusable by script only
- Trap focus in modals (Tab wraps to first/last)
- Restore focus when closing modals
- Handle Escape key to close modals/dropdowns
- Test keyboard-only navigation thoroughly

### MUST NOT DO

- Remove focus indicators without replacement (breaks keyboard access)
- Use `tabindex > 0` (causes confusion; fix DOM order instead)
- Trap focus without providing escape (Escape key must work)
- Assume mouse users find all interactive elements
- Leave modals without focus management
- Skip testing keyboard navigation in QA
- Use `onBlur` to prevent focus management (breaks assistive tech)

---

## Common Keyboard Navigation Bugs

### Bug 1: No Visible Focus Indicator

```css
/* ❌ BAD: Focus invisible -->
button:focus { outline: none; }

/* ✅ FIX: Provide visible indicator -->
button:focus-visible { outline: 3px solid #2563eb; }
```

### Bug 2: Keyboard Trap (Can't Escape)

```typescript
// ❌ BAD: Tab traps in modal, Escape doesn't work
function Modal() {
  return <div role="dialog"><!-- content --></div>;
}

// ✅ FIX: Add focus trap + Escape handler
function Modal() {
  const ref = useFocusTrap(true);
  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === 'Escape') onClose();
  };
  return <div ref={ref} role="dialog" onKeyDown={handleKeyDown}>...</div>;
}
```

### Bug 3: Focus Not Restored

```typescript
// ❌ BAD: Opening modal loses track of trigger button
<button onClick={() => setOpen(true)}>Open</button>

// ✅ FIX: Save and restore focus
const { saveFocus, restoreFocus } = useRestoreFocus();

const handleOpen = () => {
  saveFocus();
  setOpen(true);
};

const handleClose = () => {
  setOpen(false);
  restoreFocus();
};
```

---

## Testing Keyboard Navigation

### Manual Testing Steps

1. **Tab through entire page** — Verify order matches visual layout
2. **Use only keyboard** — Close trackpad, use only keyboard
3. **Test modals** — Tab in modal, Escape closes, focus restores
4. **Test menus** — Arrow keys navigate, Enter selects, Escape closes
5. **Test lists** — Home/End jump to edges, Arrow keys navigate
6. **Verify focus visible** — Can always see current focus

### Automated Testing

```typescript
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

test('keyboard navigation works in menu', async () => {
  const user = userEvent.setup();
  render(
    <Menu items={['Option 1', 'Option 2', 'Option 3']} />
  );

  const trigger = screen.getByRole('button', { name: /open menu/i });

  // Open menu with keyboard
  trigger.focus();
  await user.keyboard('{Enter}');

  // Navigate with arrow keys
  await user.keyboard('{ArrowDown}');
  expect(screen.getByText('Option 2')).toHaveFocus();

  await user.keyboard('{ArrowUp}');
  expect(screen.getByText('Option 1')).toHaveFocus();

  // Close with Escape
  await user.keyboard('{Escape}');
  expect(trigger).toHaveFocus();
});
```

Attribution

paulpaspaulpas
View sourceSee grades on GitHubMore from paulpas →
SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Related Skills

Clean Code

Pragmatic coding standards - concise, direct, no over-engineering, no unnecessary comments

304955 votes

Browser Extension Developer

Use this skill when developing or maintaining browser extension code in the `browser/` directory, including Chrome/Firefox/Edge compatibility, content scripts, background scripts, or i18n updates.

286712 votes

Seo Optimizer

SEO optimization with keyword analysis, readability assessment, technical validation, content quality. Use for search rankings, blog posts, content audits, or encountering keyword density, readability scores, meta tags, schema markup errors.

2222 votes

Google Official Seo Guide

Official Google SEO guide covering search optimization, best practices, Search Console, crawling, indexing, and improving website search visibility based on official Google documentation

1862 votes

Writing Plans

Use when you have a spec or requirements for a multi-step task, before touching code

2927051 votes
View all in development →