KeyUX

repository·main·Indexed 19 days ago

https://github.com/ai/keyux

A lightweight (2 KB) JavaScript library for improving keyboard accessibility and professional tool UX in web applications. KeyUX provides hotkey management via aria-keyshortcuts, visual pressed states, arrow-key navigation for ARIA roles (menu, listbox, tablist, toolbar), and a focusgroup attribute polyfill. It includes utilities for generating platform-aware hotkey hints, managing focus jumps with aria-controls, and supporting nested menus.

Tokens
3.4K
Snippets
14
Records
14
Agent score
19%

What's inside keyux

  1. Use Focus Groups for Arrow Navigation

    main

    KeyUX provides a polyfill for the focusgroup attribute, allowing users to navigate groups of elements using arrow keys instead of just Tab.

    Supported Attributes:

    • focusgroup: Marks a group for arrow navigation.
    • focusgroup="block": Enables vertical arrow navigation.
    • focusgroup="wrap": Enables cyclic focus movement (wraps around).
    • focusgroup="no-memory": Prevents restoring the last focus position.
    • focusgroup="none": Excludes the element from arrow navigation.

    Note: grid functionality is not currently supported.

    Setup: Call focusGroupPolyfill() in startKeyUX and use focusGroupKeyUX() to enable arrow navigation for standard ARIA roles.

    <div focusgroup="wrap">
      <button type="button">Option 1</button>
      <button type="button">Option 2</button>
    </div>
    import { focusGroupPolyfill, focusGroupKeyUX, startKeyUX } from 'keyux'
    
    startKeyUX(window, [
      focusGroupKeyUX(),
      focusGroupPolyfill()
    ])
  2. Enable Arrow Navigation for ARIA Roles (Menu, Listbox, Tablist, Toolbar)

    main

    By calling focusGroupKeyUX(), KeyUX enables arrow-key, Home, and End navigation for the following ARIA roles:

    • role="menu": Navigates through role="menuitem". Supports typing the first character of an item to jump to it.
    • role="listbox": Navigates through role="option" items.
    • role="tablist": Navigates through role="tab" elements.
    • role="toolbar": Navigates through buttons within the toolbar.

    Example (Menu):

    <nav role="menu">
      <a href="/" role="menuitem">Home</a>
      <a href="/catalog" role="menuitem">Catalog</a>
    </nav>
  3. Show a Pressed State for Hotkeys

    main

    To make the UI more responsive, you can use pressKeyUX to automatically apply a CSS class to a button when its hotkey is pressed.

    1. Initialize pressKeyUX with your desired class name (e.g., 'is-pressed').
    2. Add CSS rules for that class to handle the visual state (e.g., transform: scale(0.95)).
    import { pressKeyUX, hotkeyKeyUX, startKeyUX } from 'keyux'
    
    startKeyUX(window, [
      pressKeyUX('is-pressed'), 
      hotkeyKeyUX()
    ])
    button {
      &:active, 
      &.is-pressed {
        transform: scale(0.95);
      }
    }
  4. Implement Hotkeys with aria-keyshortcuts

    main

    KeyUX enables keyboard shortcuts by listening for the pattern defined in the aria-keyshortcuts attribute. When the matching key combination is pressed, KeyUX will trigger a click on the element.

    Requirements:

    • The hotkey pattern must include modifiers in this exact order: meta+ctrl+alt+shift+key (e.g., meta+ctrl+alt+shift+b).
    • To ignore hotkeys in specific sections (like when a dialog is open), wrap those sections in an element with the inert or aria-hidden attribute.

    Example usage:

    <!-- Triggers click on Alt+B or ⌥B -->
    <button aria-keyshortcuts="alt+b">Bold</button>
    
    <!-- Moves focus to the search input -->
    <input type="search" aria-keyshortcuts="s" placeholder="S" />
    <button aria-keyshortcuts="alt+b">Bold</button>
  5. Implement Hotkeys for List Items

    main

    To provide unique hotkeys for items within a list (e.g., an 'Add to cart' button for each product):

    1. Add data-keyux-ignore-hotkeys to the list item (<li>) to prevent the global hotkey search from finding the item's internal buttons.
    2. Make the list item focusable using tabindex="0". When the item is focused, KeyUX will allow the internal hotkeys to work.
    3. (Optional) If the action is in a separate panel, use data-keyux-hotkeys="[PANEL_ID]" on the list item to link it to that panel.

    Example:

    <li data-keyux-ignore-hotkeys tabIndex={0}>
      {product.title}
      <button aria-keyshortcuts="a" tabIndex={-1}>
        Add to card
      </button>
    </li>
  6. Override Hotkeys

    main

    Users can override default hotkeys to avoid conflicts with browser extensions or system settings. Use hotkeyOverrides() to create a transformer that maps a new key combination to an existing one.

    Example: Mapping alt+b to just b.

    import { hotkeyOverrides, hotkeyKeyUX, startKeyUX, getHotKeyHint } from 'keyux'
    
    // Define the mapping: 'new_combination': 'original_shortcut'
    const config = { 'alt+b': 'b' }
    const overrides = hotkeyOverrides(config)
    
    startKeyUX(window, [
      hotkeyKeyUX([overrides])
    ])
    
    // The hint will now correctly show 'Alt + B' for the 'b' shortcut
    getHotKeyHint(window, 'b', [overrides])
    const overrides = {
      'alt+b': 'b' 
    }
    
    startKeyUX(window, [
      hotkeyKeyUX([hotkeyOverrides(overrides)])
    ])
  7. Enable Mac Compatibility (Meta instead of Ctrl)

    main

    On macOS, users often expect the Meta (⌘) key to be used for shortcuts instead of Ctrl. You can enable this behavior using hotkeyMacCompat().

    When enabled, pressing Meta will be treated as if Ctrl was pressed, ensuring a familiar experience across platforms.

    import { hotkeyMacCompat, hotkeyKeyUX, startKeyUX, getHotKeyHint } from 'keyux'
    
    const mac = hotkeyMacCompat()
    startKeyUX(window, [hotkeyKeyUX([mac])])
    
    // Returns '⌘+b' on Mac and 'Ctrl+B' on Windows/Linux
    getHotKeyHint(window, 'ctrl+b', [mac])
  8. Create Nested Menus with hiddenKeyUX

    main

    KeyUX supports nested menus by leveraging aria-controls and aria-hidden="true". To create a nested structure, the sub-menu should be marked with hidden and aria-hidden="true". When the parent element triggers the sub-menu via aria-controls, KeyUX handles the visibility.

    Note: When making a nested menu visible, you must manually set tabindex="-1" on the target element if required by your specific accessibility needs.

    To enable nested menu functionality, you must call hiddenKeyUX() within your startKeyUX initialization.

    import { focusGroupKeyUX, jumpKeyUX, hiddenKeyUX } from 'keyux'
    
    // Initialize KeyUX with hiddenKeyUX to enable nested menu support
    startKeyUX(window, [focusGroupKeyUX(), jumpKeyUX(), hiddenKeyUX()])
    
    /* 
    Example HTML Structure:
    <button aria-controls="edit" aria-haspopup="menu">Edit</button>
    
    <div id="edit" hidden aria-hidden="true" role="menu">
      <button role="menuitem">Undo</button>
      <button role="menuitem" aria-controls="find" aria-haspopup="menu">
        Find
      </button>
    </div>
    
    <div id="find" hidden aria-hidden="true" role="menu">
      <button role="menuitem">Find…</button>
      <button role="menuitem">Replace…</button>
    </div>
    */
  9. Install KeyUX via npm

    main

    Install the keyux package using npm. After installation, you must initialize the library by calling startKeyUX in your main JavaScript entry point, passing the window object and an array of desired features.

    npm install keyux
    import {
      hotkeyKeyUX,
      focusGroupKeyUX,
      focusGroupPolyfill,
      pressKeyUX,
      jumpKeyUX,
      hiddenKeyUX,
      startKeyUX
    } from 'keyux'
    
    startKeyUX(window, [
      hotkeyKeyUX(),
      focusGroupKeyUX(),
      focusGroupPolyfill(),
      pressKeyUX('is-pressed'),
      jumpKeyUX(),
      hiddenKeyUX()
    ])
  10. Implement Focus Jumps with aria-controls

    main

    You can automate focus movement between UI sections using the aria-controls attribute. When a user interacts with an element (e.g., selecting a menu item or pressing <kbd>Enter</kbd> on an <input>), focus will jump to the element identified by the ID in aria-controls. Pressing <kbd>Esc</kbd> will automatically jump the focus back to the original element.

    To enable this behavior, you must call jumpKeyUX() within your startKeyUX initialization.

    import { focusGroupKeyUX, jumpKeyUX } from 'keyux'
    
    // Initialize KeyUX with jumpKeyUX to enable focus jumping
    startKeyUX(window, [focusGroupKeyUX(), jumpKeyUX()])
    
    // Example HTML usage:
    // 1. For menu items:
    // <button role="menuitem" aria-controls="target_id">Item</button>
    // 2. For inputs (jumps on Enter):
    // <input type="search" aria-controls="search_results" />
  11. Display pretty Hotkey Hints

    main

    Use getHotKeyHint() to transform raw aria-keyshortcuts strings into user-friendly text (e.g., converting alt+b to Alt + B on Windows or ⌥ B on Mac). Use likelyWithKeyboard() to detect if the user is on a device where keyboard shortcuts are unlikely (like mobile) to conditionally hide hints.

    Note: If using hotkeyOverrides, you must pass the same override configuration to both hotkeyKeyUX() and getHotKeyHint() to ensure the displayed hint matches the actual behavior.

    import { likelyWithKeyboard, getHotKeyHint } from 'keyux'
    
    export const Button = ({ hotkey, children }) => {
      return (
        <button aria-keyshortcuts={hotkey}>
          {children}
          {likelyWithKeyboard(window) && <kbd>{getHotKeyHint(window, hotkey)}</kbd>}
        </button>
      )
    }
  12. Detect keyboard availability with likelyWithKeyboard()

    main

    The likelyWithKeyboard(window) function returns a boolean indicating whether the user is likely using a device with a physical keyboard. It checks the navigator.userAgent and returns false if the device is identified as 'iphone', 'ipad', or 'android'. Defaults to globalThis if no window is provided.

    import { likelyWithKeyboard } from 'keyux';
    
    if (likelyWithKeyboard()) {
      // Enable keyboard-specific UI or shortcuts
    }