keyboard navigation

Ensure all functionality is accessible via keyboard for users who cannot or do not use a mouse, including those with motor disabilities and power users.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "keyboard navigation" with this command: npx skills add dasien/retrowarden/dasien-retrowarden-keyboard-navigation

Keyboard Navigation

Purpose

Ensure all functionality is accessible via keyboard for users who cannot or do not use a mouse, including those with motor disabilities and power users.

When to Use

  • Building interactive UIs

  • Implementing modals, dropdowns, menus

  • Creating custom controls

  • Accessibility testing

  • Keyboard shortcut implementation

Key Capabilities

  • Focus Management - Ensure logical focus order and visible indicators

  • Keyboard Shortcuts - Implement accessible shortcuts

  • Focus Trapping - Manage focus in modals and dialogs

Approach

Enable Keyboard Access

  • All interactive elements focusable

  • Logical tab order (left-to-right, top-to-bottom)

  • No keyboard traps (can escape any control)

  • Custom controls keyboard accessible

Implement Standard Keys

  • Tab: Move forward through interactive elements

  • Shift+Tab: Move backward

  • Enter: Activate buttons, submit forms, follow links

  • Space: Activate buttons, checkboxes, toggle controls

  • Escape: Close modals, cancel operations, clear selections

  • Arrow keys: Navigate within components (menus, tabs, lists)

  • Home/End: Jump to start/end of content

Provide Visible Focus Indicators

  • Clear outline or ring

  • Sufficient contrast (3:1 minimum)

  • Consistent across site

  • Never remove without replacement

Manage Focus

  • Set focus to modals when opened

  • Restore focus when closed

  • Trap focus in modals

  • Skip navigation links

Test Keyboard Navigation

  • Unplug mouse

  • Navigate entire site with keyboard only

  • Verify all functionality accessible

  • Check focus indicators visible

Example

Context: Accessible modal dialog with focus trap

class AccessibleModal { constructor(modalElement) { this.modal = modalElement; this.focusableElements = null; this.firstFocusable = null; this.lastFocusable = null; this.previousFocus = null;

    // Bind event handlers
    this.handleKeyDown = this.handleKeyDown.bind(this);
}

open() {
    // Store current focus to restore later
    this.previousFocus = document.activeElement;
    
    // Show modal
    this.modal.style.display = 'block';
    this.modal.setAttribute('aria-hidden', 'false');
    
    // Get all focusable elements
    this.focusableElements = this.modal.querySelectorAll(
        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );
    
    this.firstFocusable = this.focusableElements[0];
    this.lastFocusable = this.focusableElements[this.focusableElements.length - 1];
    
    // Focus first element or modal itself
    if (this.firstFocusable) {
        this.firstFocusable.focus();
    } else {
        this.modal.focus();
    }
    
    // Trap focus
    this.modal.addEventListener('keydown', this.handleKeyDown);
    
    // Prevent body scroll
    document.body.style.overflow = 'hidden';
}

close() {
    // Hide modal
    this.modal.style.display = 'none';
    this.modal.setAttribute('aria-hidden', 'true');
    
    // Remove event listener
    this.modal.removeEventListener('keydown', this.handleKeyDown);
    
    // Restore focus to previous element
    if (this.previousFocus) {
        this.previousFocus.focus();
    }
    
    // Restore body scroll
    document.body.style.overflow = '';
}

handleKeyDown(e) {
    // Trap focus within modal
    if (e.key === 'Tab') {
        if (e.shiftKey) {
            // Shift+Tab: backward
            if (document.activeElement === this.firstFocusable) {
                e.preventDefault();
                this.lastFocusable.focus();
            }
        } else {
            // Tab: forward
            if (document.activeElement === this.lastFocusable) {
                e.preventDefault();
                this.firstFocusable.focus();
            }
        }
    }
    
    // Close on Escape
    if (e.key === 'Escape') {
        this.close();
    }
}

}

// Usage const modal = new AccessibleModal(document.getElementById('myModal'));

document.getElementById('openModal').addEventListener('click', () => { modal.open(); });

document.getElementById('closeModal').addEventListener('click', () => { modal.close(); });

HTML for Accessible Modal:

<!-- Modal trigger --> <button id="openModal" aria-haspopup="dialog" aria-expanded="false"

Open Dialog

</button>

<!-- Modal --> <div id="myModal" role="dialog" aria-modal="true" aria-labelledby="modal-title" aria-describedby="modal-description" aria-hidden="true" tabindex="-1"

&#x3C;div class="modal-content">
    &#x3C;h2 id="modal-title">Confirm Action&#x3C;/h2>
    &#x3C;p id="modal-description">
        Are you sure you want to delete this item?
    &#x3C;/p>
    
    &#x3C;div class="modal-actions">
        &#x3C;button id="confirmButton">
            Confirm
        &#x3C;/button>
        &#x3C;button id="closeModal">
            Cancel
        &#x3C;/button>
    &#x3C;/div>
&#x3C;/div>

</div>

<style> /* Visible focus indicators */ button:focus, a:focus, input:focus { outline: 3px solid #0066cc; outline-offset: 2px; }

/* Focus within modal */
.modal-content *:focus {
    outline-color: #0066cc;
}

</style>

Accessible Dropdown Menu:

class AccessibleDropdown { constructor(button, menu) { this.button = button; this.menu = menu; this.menuItems = menu.querySelectorAll('[role="menuitem"]'); this.currentIndex = -1;

    this.button.addEventListener('click', () => this.toggle());
    this.button.addEventListener('keydown', (e) => this.handleButtonKey(e));
    this.menu.addEventListener('keydown', (e) => this.handleMenuKey(e));
    
    // Close on outside click
    document.addEventListener('click', (e) => {
        if (!this.button.contains(e.target) &#x26;&#x26; !this.menu.contains(e.target)) {
            this.close();
        }
    });
}

toggle() {
    if (this.menu.style.display === 'block') {
        this.close();
    } else {
        this.open();
    }
}

open() {
    this.menu.style.display = 'block';
    this.button.setAttribute('aria-expanded', 'true');
    this.currentIndex = 0;
    this.menuItems[0].focus();
}

close() {
    this.menu.style.display = 'none';
    this.button.setAttribute('aria-expanded', 'false');
    this.button.focus();
    this.currentIndex = -1;
}

handleButtonKey(e) {
    // Open on Enter, Space, or Down Arrow
    if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') {
        e.preventDefault();
        this.open();
    }
}

handleMenuKey(e) {
    switch (e.key) {
        case 'ArrowDown':
            e.preventDefault();
            this.currentIndex = (this.currentIndex + 1) % this.menuItems.length;
            this.menuItems[this.currentIndex].focus();
            break;
        
        case 'ArrowUp':
            e.preventDefault();
            this.currentIndex = this.currentIndex - 1;
            if (this.currentIndex &#x3C; 0) {
                this.currentIndex = this.menuItems.length - 1;
            }
            this.menuItems[this.currentIndex].focus();
            break;
        
        case 'Home':
            e.preventDefault();
            this.currentIndex = 0;
            this.menuItems[0].focus();
            break;
        
        case 'End':
            e.preventDefault();
            this.currentIndex = this.menuItems.length - 1;
            this.menuItems[this.currentIndex].focus();
            break;
        
        case 'Escape':
            e.preventDefault();
            this.close();
            break;
        
        case 'Enter':
        case ' ':
            e.preventDefault();
            this.menuItems[this.currentIndex].click();
            this.close();
            break;
    }
}

}

Skip Navigation Link:

<!-- Skip link (first focusable element) --> <a href="#main-content" class="skip-link"> Skip to main content </a>

<!-- Navigation --> <nav> <!-- Many navigation links --> </nav>

<!-- Main content --> <main id="main-content" tabindex="-1"> <!-- Page content --> </main>

<style> /* Hidden by default, visible on focus */ .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 Shortcuts:

// Global keyboard shortcuts document.addEventListener('keydown', (e) => { // Ignore if typing in input if (e.target.matches('input, textarea')) { return; }

// Ctrl/Cmd + K: Search
if ((e.ctrlKey || e.metaKey) &#x26;&#x26; e.key === 'k') {
    e.preventDefault();
    document.getElementById('search-input').focus();
}

// ? : Show keyboard shortcuts
if (e.key === '?') {
    e.preventDefault();
    showKeyboardShortcuts();
}

// Esc: Close any open modals
if (e.key === 'Escape') {
    closeAllModals();
}

});

// Show keyboard shortcuts dialog function showKeyboardShortcuts() { const shortcuts = [ { keys: 'Ctrl+K', action: 'Open search' }, { keys: '?', action: 'Show keyboard shortcuts' }, { keys: 'Esc', action: 'Close dialog' }, { keys: 'Tab', action: 'Next element' }, { keys: 'Shift+Tab', action: 'Previous element' }, ];

// Display shortcuts in accessible modal
showModal({
    title: 'Keyboard Shortcuts',
    content: renderShortcuts(shortcuts)
});

}

Testing Keyboard Navigation:

Testing Checklist

Basic Navigation

  • Tab moves forward through all interactive elements
  • Shift+Tab moves backward
  • Focus indicator always visible
  • Tab order is logical (reading order)
  • No keyboard traps (can escape every control)

Interactive Elements

  • Enter activates buttons and links
  • Space activates buttons and checkboxes
  • Arrow keys navigate dropdowns/menus
  • Escape closes modals and menus
  • Custom controls have keyboard support

Forms

  • Tab moves between form fields
  • Arrow keys work in radio groups
  • Space toggles checkboxes
  • Enter submits form
  • Error messages reachable via keyboard

Modals

  • Focus moves to modal when opened
  • Tab cycles within modal (focus trap)
  • Escape closes modal
  • Focus returns to trigger on close

Navigation

  • Skip link works
  • All navigation items reachable
  • Current page indicated
  • Submenus keyboard accessible

Custom Controls

  • Appropriate ARIA roles
  • Keyboard interactions documented
  • All states keyboard accessible
  • Focus management correct

Best Practices

  • ✅ All interactive elements keyboard accessible

  • ✅ Visible focus indicators (3px outline minimum)

  • ✅ Logical tab order (no tabindex > 0)

  • ✅ Escape closes modals/menus

  • ✅ Arrow keys for menus/lists/tabs

  • ✅ Home/End for start/end navigation

  • ✅ Skip navigation links

  • ✅ Focus trapping in modals

  • ✅ Restore focus after closing modals

  • ✅ Document keyboard shortcuts

  • ❌ Avoid: Keyboard traps (can't escape)

  • ❌ Avoid: Removing focus outline without replacement

  • ❌ Avoid: Using tabindex > 0 (breaks natural order)

  • ❌ Avoid: Non-standard keyboard interactions

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

data quality

No summary provided by upstream source.

Repository SourceNeeds Review
General

image-gen

Generate AI images from text prompts. Triggers on: "生成图片", "画一张", "AI图", "generate image", "配图", "create picture", "draw", "visualize", "generate an image".

Archived SourceRecently Updated
General

explainer

Create explainer videos with narration and AI-generated visuals. Triggers on: "解说视频", "explainer video", "explain this as a video", "tutorial video", "introduce X (video)", "解释一下XX(视频形式)".

Archived SourceRecently Updated
General

asr

Transcribe audio files to text using local speech recognition. Triggers on: "转录", "transcribe", "语音转文字", "ASR", "识别音频", "把这段音频转成文字".

Archived SourceRecently Updated