Introduction

Add keyboard navigation to lists, grids and trees with one keydown handler, then choose keys and options while native Tab behavior keeps working.

keyrove moves focus through lists, grids and trees. Mark the items you want to navigate and pass keydown events to keyRove. It finds and focuses the next item without rendering UI or wrapping your components.

<ul id="menu">
  <li data-keyrove-item tabindex="0">Inbox</li>
  <li data-keyrove-item tabindex="0">Drafts</li>
  <li data-keyrove-item tabindex="0">Sent</li>
</ul>

This list supports arrows, Home, End and page jumps. Tab still visits each item. You can change the keys and configure the group with attributes, options, or both.

How it works

  1. Items are the elements marked with data-keyrove-item, read in DOM order. An items option can select them instead.
  2. The root is the nearest element at or above the event target marked with data-keyrove-root, or the listener's element if none is marked. It contains the items and carries the group's settings.
  3. The handler, keyRove(event), reads the key, finds the target item and focuses it. It calls preventDefault() for handled keys, so navigation does not also scroll the page. Unhandled keys keep their browser defaults.

The handler reads the current DOM and settings on every keypress. Changing items or adding a column count requires no navigation instance to update.

What it handles

FeatureBehaviorExample
Lists↑/↓ move one item in DOM orderBasic list
Grids↑/↓ move a row; ←/→ move a cellGrid
First and last itemsHome/End jump to list ends or grid row ends; Ctrl+Home/End jump to grid endsGrid
Page jumpsPageUp/PageDown move 10 items or rows by defaultPage length
Horizontal lists←/→ follow the text directionHorizontal lists
LoopingNext/previous wrap at list endsLooping lists
Roving tabindexOne tab stop follows focus within a groupRoving tabindex
Skipped itemsPass over items marked with data-keyrove-skip or disabledSkipped items
Nested groupsEach group has its own keys, with optional enter and exit bindingsNested roots
Focus shortcutsFocus an item or panel from anywhere under the listenerFocus keys
TypeaheadAdd createTypeahead() to focus items by typing their labelsTypeahead
TreesNavigate visible rows; your handlers expand and collapse branchesTree view
Moves from codeCall rove(list, 'next') from a button, gamepad or remoterove

Text fields, selects and editable content keep their editing keys. See editable targets for the rules and exceptions.

Where a group is described

Use data-keyrove-* attributes in your markup or pass options to the handler. Options are useful when the HTML comes from a component library or CMS:

import { keyRove } from '@mixedrays/keyrove';

const config = { items: '[role="menuitem"]', loop: true };
const menu = document.querySelector<HTMLElement>('#share')!;

menu.addEventListener('keydown', (e) => keyRove(e, config));

Options override attributes one setting at a time. For example, keyRove(e, { loop: true }) enables looping while reading items and key bindings from the markup.

See attributes and options for the full mapping, or options in JavaScript for a complete demo.

Which keys move focus

Lists use ArrowDown for next and ArrowUp for previous by default. Set data-keyrove-next-key and data-keyrove-prev-key on the root to use other KeyboardEvent.code values or combinations such as mod+KeyJ:

<ul data-keyrove-next-key="KeyJ" data-keyrove-prev-key="KeyK">
  …
</ul>

Here, J and K move focus, and the arrows return to their browser defaults. Each group can have different bindings. Home, End, PageUp and PageDown can also be rebound with their own key attributes.

See custom keys for examples and the API reference for defaults, syntax and precedence.

Tab still works

keyrove uses element.focus(), preserving the browser's focus styling, scrolling and focus announcements. An item also counts as focused when a link, button or other element inside it has focus.

With the default bindings, Tab, Shift+Tab, Enter, Space and Escape keep their usual behavior. Items with tabindex="0" remain ordinary tab stops, reachable with both Tab and the bound navigation keys.

For a single tab stop per group, use roving tabindex. Tab enters and leaves the group, while the bound keys move between its items.

What it leaves to you

  • Roles and ARIA. Set the roles, labels and states your widget needs. keyrove does not write role, aria-selected or aria-activedescendant.
  • Selection and activation. Decide what clicks, Enter and Space do, and add your own handlers. They are unbound by default.
  • Initial tab stops. Make items focusable in your markup. For a roving group, give one item tabindex="0" and the others -1, or call initRovingTabindex after rendering.

The listbox example combines navigation with all three.

Framework support

keyRove accepts native keyboard events and compatible framework events, including React's synthetic events. See KeyRoveEvent for the required shape.

Installation shows setup in vanilla JavaScript, React, Vue and Svelte. Start with the basic list to try it.