Keyboard Navigation | Blue Frog Docs

Keyboard Navigation

Diagnose and fix keyboard accessibility issues to ensure all functionality is usable without a mouse

Keyboard Navigation

What This Means

Keyboard navigation ensures that all website functionality can be accessed and operated using only a keyboard, without requiring a mouse or trackpad. This is essential for users who cannot use a mouse due to motor disabilities, vision impairments, or those who simply prefer keyboard navigation for efficiency.

Impact on Your Business

Legal Compliance:

  • Keyboard accessibility is required under WCAG 2.1 Level A
  • One of the most fundamental accessibility requirements
  • Failure to support keyboard navigation is a critical violation
  • Required for ADA and Section 508 compliance

User Populations Affected:

  • Users with motor disabilities - Cannot use a mouse
  • Blind and low-vision users - Navigate with screen readers
  • Power users - Prefer keyboard for efficiency
  • Temporary disabilities - Broken arm, RSI
  • Potentially millions of users excluded

Business Impact:

  • Keyboard-inaccessible sites exclude entire user segments
  • Critical functionality (checkout, forms) may be unusable
  • Increases bounce rates for affected users
  • Damages brand reputation and trust

SEO and Technical Benefits:

  • Semantic HTML improves SEO
  • Better code structure and maintainability
  • Improved compatibility with assistive technologies
  • Benefits mobile and touch device users

How to Diagnose

This is the most important test:

  1. Close or ignore your mouse/trackpad

  2. Navigate using only keyboard:

    • Tab - Move to next interactive element
    • Shift+Tab - Move to previous interactive element
    • Enter - Activate links and buttons
    • Space - Activate buttons, check checkboxes
    • Arrow keys - Navigate within components (dropdowns, radio groups)
    • Esc - Close modals and menus
  3. Check every page section:

    • Can you reach all links?
    • Can you activate all buttons?
    • Can you use all forms?
    • Can you close modals?
    • Can you use dropdown menus?

What to Look For:

  • Elements you cannot reach with Tab
  • Missing or invisible focus indicators
  • Illogical tab order
  • Keyboard traps (can't Tab out)
  • Elements that require mouse (hover-only)

Method 2: WAVE Browser Extension

  1. Install WAVE Extension
  2. Navigate to your webpage
  3. Click WAVE icon
  4. Review errors:
    • "Missing form label"
    • "Empty button"
    • "Empty link"
  5. Check for structural issues:
    • "Heading" hierarchy
    • "Landmark" regions

What to Look For:

  • Links without text
  • Buttons without labels
  • Form inputs without labels
  • Broken heading hierarchy
  • Missing ARIA labels

Method 3: axe DevTools

  1. Install axe DevTools Extension
  2. Open Chrome DevTools (F12)
  3. Navigate to "axe DevTools" tab
  4. Click "Scan ALL of my page"
  5. Review keyboard-related violations:
    • "Elements must have sufficient color contrast"
    • "Links must have discernible text"
    • "Buttons must have discernible text"
    • "Form elements must have labels"

What to Look For:

  • Interactive elements without accessible names
  • Missing focus indicators
  • Keyboard trap issues
  • ARIA role violations

Method 4: Chrome DevTools Accessibility Tab

  1. Open your website
  2. Press F12 to open DevTools
  3. Right-click an element → Inspect
  4. In Elements panel, click "Accessibility" tab
  5. Review:
    • Computed Properties - Name, Role, Focusable
    • Accessibility Tree - How element appears to assistive tech

What to Look For:

  • Focusable: "false" on interactive elements
  • Missing accessible name
  • Incorrect role
  • Elements not in accessibility tree

Method 5: Focus Indicator Test

  1. Enable Chrome DevTools > Rendering
  2. Check "Emulate a focused page"
  3. Tab through the page
  4. Observe focus indicators

What to Look For:

  • Invisible focus indicators (no visual change)
  • Focus indicators removed by CSS
  • Low contrast focus indicators
  • Focus jumping unexpectedly

General Fixes

Fix 1: Make All Interactive Elements Focusable

Ensure elements can receive keyboard focus:

  1. Use proper HTML elements:

    <!-- Bad - div is not focusable by default -->
    <div onclick="handleClick()">Click me</div>
    
    <!-- Good - button is focusable -->
    <button onclick="handleClick()">Click me</button>
    
  2. If you must use non-button elements, add tabindex and role:

    <!-- Only do this if you can't use a real button -->
    <div
      role="button"
      tabindex="0"
      onclick="handleClick()"
      onkeydown="handleKeyDown(event)"
    >
      Click me
    </div>
    
    <script>
    function handleKeyDown(event) {
      if (event.key === 'Enter' || event.key === ' ') {
        event.preventDefault();
        handleClick();
      }
    }
    </script>
    
  3. Use links for navigation, buttons for actions:

    <!-- Link - goes somewhere -->
    <a href="/products">View Products</a>
    
    <!-- Button - does something -->
    <button type="button" onclick="openModal()">Open Details</button>
    

Fix 2: Provide Visible Focus Indicators

Make it obvious which element has focus:

  1. Never remove focus outlines without replacement:

    /* Bad - removes focus with no replacement */
    *:focus {
      outline: none;
    }
    
    /* Good - custom focus indicator */
    button:focus,
    a:focus,
    input:focus {
      outline: 2px solid #0066cc;
      outline-offset: 2px;
    }
    
  2. Use high contrast focus styles:

    /* Ensure focus visible on all backgrounds */
    :focus {
      outline: 2px solid #0066cc;
      outline-offset: 2px;
    }
    
    /* For dark backgrounds */
    .dark-theme :focus {
      outline: 2px solid #66b3ff;
    }
    
  3. Use :focus-visible for mouse vs keyboard:

    /* Only show outline for keyboard focus */
    button:focus-visible {
      outline: 2px solid #0066cc;
      outline-offset: 2px;
    }
    
    /* Remove outline for mouse clicks */
    button:focus:not(:focus-visible) {
      outline: none;
    }
    
  4. Enhanced focus styles:

    button:focus {
      outline: 2px solid #0066cc;
      outline-offset: 2px;
      box-shadow: 0 0 0 4px rgba(0, 102, 204, 0.2);
    }
    

Fix 3: Fix Tab Order

Ensure logical navigation sequence:

  1. Use natural DOM order (no tabindex > 0):

    <!-- Bad - forces unnatural order -->
    <button tabindex="3">Third</button>
    <button tabindex="1">First</button>
    <button tabindex="2">Second</button>
    
    <!-- Good - natural DOM order -->
    <button>First</button>
    <button>Second</button>
    <button>Third</button>
    
  2. Use tabindex values correctly:

    • tabindex="0" - Natural tab order (focusable)
    • tabindex="-1" - Programmatically focusable (not in tab order)
    • tabindex="1+" - Avoid! Creates unpredictable order
  3. Structure HTML in reading order:

    <!-- Good - header, nav, main, footer -->
    <header>
      <nav><!-- navigation --></nav>
    </header>
    <main>
      <h1>Page Title</h1>
      <!-- main content -->
    </main>
    <footer><!-- footer --></footer>
    

Fix 4: Prevent Keyboard Traps

Users must be able to navigate away from all elements:

  1. Test modals for keyboard traps:

    // Trap focus within modal
    function trapFocus(element) {
      const focusableElements = element.querySelectorAll(
        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
      );
      const firstFocusable = focusableElements[0];
      const lastFocusable = focusableElements[focusableElements.length - 1];
    
      element.addEventListener('keydown', (e) => {
        if (e.key !== 'Tab') return;
    
        if (e.shiftKey) {
          // Shift + Tab
          if (document.activeElement === firstFocusable) {
            lastFocusable.focus();
            e.preventDefault();
          }
        } else {
          // Tab
          if (document.activeElement === lastFocusable) {
            firstFocusable.focus();
            e.preventDefault();
          }
        }
      });
    }
    
  2. Ensure modals can be closed with Escape:

    modal.addEventListener('keydown', (e) => {
      if (e.key === 'Escape') {
        closeModal();
      }
    });
    
  3. Return focus after modal closes:

    function openModal() {
      lastFocusedElement = document.activeElement;
      modal.style.display = 'block';
      modal.querySelector('button').focus();
    }
    
    function closeModal() {
      modal.style.display = 'none';
      lastFocusedElement.focus();
    }
    

Fix 5: Make Dropdowns and Menus Keyboard Accessible

Hover-only menus exclude keyboard users:

  1. Add keyboard event handlers:

    const menuButton = document.querySelector('.menu-button');
    const menu = document.querySelector('.menu');
    
    // Show on click
    menuButton.addEventListener('click', () => {
      menu.classList.toggle('open');
    });
    
    // Show on Enter/Space
    menuButton.addEventListener('keydown', (e) => {
      if (e.key === 'Enter' || e.key === ' ') {
        e.preventDefault();
        menu.classList.toggle('open');
      }
    });
    
    // Navigate menu items with arrow keys
    menu.addEventListener('keydown', (e) => {
      const items = menu.querySelectorAll('a');
      const currentIndex = Array.from(items).indexOf(document.activeElement);
    
      if (e.key === 'ArrowDown') {
        e.preventDefault();
        const next = items[currentIndex + 1] || items[0];
        next.focus();
      } else if (e.key === 'ArrowUp') {
        e.preventDefault();
        const prev = items[currentIndex - 1] || items[items.length - 1];
        prev.focus();
      } else if (e.key === 'Escape') {
        menu.classList.remove('open');
        menuButton.focus();
      }
    });
    
  2. Use ARIA for dropdown state:

    <button
      aria-expanded="false"
      aria-controls="dropdown-menu"
      onclick="toggleMenu()"
    >
      Menu
    </button>
    <ul id="dropdown-menu" hidden>
      <li><a href="#1">Item 1</a></li>
      <li><a href="#2">Item 2</a></li>
    </ul>
    
    <script>
    function toggleMenu() {
      const button = document.querySelector('[aria-controls="dropdown-menu"]');
      const menu = document.getElementById('dropdown-menu');
      const isExpanded = button.getAttribute('aria-expanded') === 'true';
    
      button.setAttribute('aria-expanded', !isExpanded);
      menu.hidden = isExpanded;
    }
    </script>
    

Fix 6: Label All Form Elements

Every input needs an associated label:

  1. Use explicit labels:

    <!-- Good - explicit association -->
    <label for="email">Email Address</label>
    <input id="email" type="email" name="email">
    
  2. Or wrap inputs in labels:

    <!-- Also good - implicit association -->
    <label>
      Email Address
      <input type="email" name="email">
    </label>
    
  3. Use aria-label for icon buttons:

    <button aria-label="Close modal">
      <svg aria-hidden="true"><!-- close icon --></svg>
    </button>
    
  4. Group related form elements:

    <fieldset>
      <legend>Shipping Address</legend>
      <label for="street">Street</label>
      <input id="street" type="text">
    
      <label for="city">City</label>
      <input id="city" type="text">
    </fieldset>
    

Allow users to skip repetitive content:

  1. Add skip to main content link:

    <body>
      <a href="#main" class="skip-link">Skip to main content</a>
    
      <header>
        <nav><!-- Navigation --></nav>
      </header>
    
      <main id="main" tabindex="-1">
        <!-- Main content -->
      </main>
    </body>
    
  2. Style skip link (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;
    }
    

Fix 8: Use Semantic HTML and ARIA

Proper structure aids navigation:

  1. Use landmark regions:

    <header role="banner">
      <nav role="navigation" aria-label="Main navigation">
        <!-- nav items -->
      </nav>
    </header>
    
    <main role="main">
      <article>
        <h1>Article Title</h1>
        <!-- content -->
      </article>
    </main>
    
    <aside role="complementary" aria-label="Related articles">
      <!-- sidebar -->
    </aside>
    
    <footer role="contentinfo">
      <!-- footer -->
    </footer>
    
  2. Use proper heading hierarchy:

    <h1>Page Title</h1>
      <h2>Section 1</h2>
        <h3>Subsection 1.1</h3>
        <h3>Subsection 1.2</h3>
      <h2>Section 2</h2>
        <h3>Subsection 2.1</h3>
    
  3. Use ARIA when needed:

    <!-- Live region for dynamic updates -->
    <div role="status" aria-live="polite">
      Items added to cart
    </div>
    
    <!-- Current page in navigation -->
    <nav>
      <a href="/">Home</a>
      <a href="/products" aria-current="page">Products</a>
      <a href="/about">About</a>
    </nav>
    

Platform-Specific Guides

Detailed implementation instructions for your specific platform:

Platform Troubleshooting Guide
Shopify Shopify Keyboard Navigation Guide
WordPress WordPress Keyboard Navigation Guide
Wix Wix Keyboard Navigation Guide
Squarespace Squarespace Keyboard Navigation Guide
Webflow Webflow Keyboard Navigation Guide

Verification

After implementing keyboard accessibility:

  1. Complete keyboard navigation test:

    • Unplug mouse
    • Navigate entire site with keyboard only
    • Verify all functionality accessible
    • Check tab order is logical
    • Confirm focus always visible
  2. Test with screen reader:

    • NVDA (Windows) or VoiceOver (Mac)
    • Navigate using screen reader shortcuts
    • Verify all elements announced correctly
    • Check ARIA labels make sense
  3. Run automated tests:

    • axe DevTools (no keyboard violations)
    • WAVE extension (no structural errors)
    • Lighthouse (passes accessibility audit)
  4. Test specific scenarios:

    • Complete a form submission
    • Open and close a modal
    • Navigate a dropdown menu
    • Use any interactive widgets
  5. Test edge cases:

    • Keyboard traps (shouldn't exist)
    • Skip links work
    • Focus management in SPAs
    • Dynamic content updates

Common Mistakes

  1. Using <div> or <span> instead of <button> - Not focusable by default
  2. Removing focus outlines - Users can't see where they are
  3. Hover-only menus - Keyboard users can't access
  4. Missing keyboard handlers - Only mouse events implemented
  5. Improper tabindex use - Using tabindex > 0
  6. Keyboard traps - Can't escape modal or widget
  7. Unlabeled form inputs - Screen readers can't identify purpose
  8. Missing skip links - Forces navigation through all content
  9. Illogical tab order - Visual order doesn't match DOM order
  10. Focus not visible - Low contrast or invisible focus indicators

Keyboard Navigation Checklist

  • All interactive elements reachable via Tab
  • Tab order is logical and follows visual order
  • Focus indicators are clearly visible
  • Focus indicators have sufficient contrast (3:1)
  • Enter/Space activate buttons and links
  • Escape closes modals and menus
  • Arrow keys work in dropdowns and menus
  • No keyboard traps exist
  • Skip links provided for main content
  • All form inputs have labels
  • Custom widgets have appropriate ARIA
  • Modals manage focus correctly
  • Dynamic content updates announced to screen readers
  • Dropdowns accessible without mouse
  • All functionality available via keyboard

Additional Resources

// SYS.FOOTER