Keyboard navigation that behaves itself.

Mark the items and pass keydown to keyRove. Lists, grids and trees then move focus the way keyboard users expect, in any framework.

Get started Examples npm i @mixedrays/keyrove

  • ~5 kB gzipped
  • zero deps
  • any framework
Live · mail folders
import { keyRove } from '@mixedrays/keyrove';

// whole setup
const menu = document.querySelector('#menu');
menu.addEventListener('keydown', (e) => keyRove(e));

↑ ↓ move, Home and End jump. Tab still visits each folder.

01 / Your stack

Use it in your framework

The same items, the same handler. keyRove accepts native keyboard events and compatible framework events, including React's synthetic events. There is no adapter or wrapper component to install.

Start with ordinary attributes. When you want type-checked settings, attribute builders fit the same templates.

Framework setup

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

const menu = document.querySelector('#menu');
menu.addEventListener('keydown', (e) => keyRove(e));

02 / Your settings

Add only the behavior you need

Keep the handler. Change a setting in the markup.

Looping wraps from the last item to the first. Horizontal changes the default arrows. One tab stop lets Tab enter and leave the group while the arrows move within it.

Each example adds just one behavior to the list you already know.

Live · looping folders
  • Inbox
  • Drafts
  • Sent
<ul id="folders-loop" aria-label="Mail folders" data-keyrove-loop>
  <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>

Press ↓ on Sent to return to Inbox.

Looping lists

03 / Your markup

Configure it where it fits

Write attributes when you own the markup. Use an item selector when the HTML comes from a component library or CMS. These two setups navigate identically.

You can mix both: keyRove(e, { loop: true }) overrides looping while still reading the items and other settings from their attributes.

Attributes and options

<ul id="folders">
  <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>
<script type="module">
  import { keyRove } from '@mixedrays/keyrove';

  const folders = document.querySelector('#folders');
  folders.addEventListener('keydown', (e) => keyRove(e));
</script>

04 / Your layout

Move through a grid

Give the group a column count and the same handler moves across cells and between rows. Your CSS still controls the layout.

For a responsive CSS grid, set data-keyrove-cols="auto". keyrove reads the current tracks on each keypress, so resizing needs no navigation instance to update.

Grid navigation

Live · time slots
<div
  id="slots"
  data-keyrove-cols="3"
  style="display: grid; grid-template-columns: repeat(3, 1fr)"
>
  <button data-keyrove-item>09:00</button>
  <button data-keyrove-item>09:30</button>
  <button data-keyrove-item>10:00</button>
  <!-- … -->
  <button data-keyrove-item>13:00</button>
</div>

Use all four arrows. Home and End jump to the ends of a row.

05 / Your interaction

Combine behaviors

Now put the pieces together: a menu configured with an item selector, looping, and one tab stop.

Add createTypeahead when people should also be able to find an item by its label. Both handlers share the same group settings; || passes a key to typeahead only when navigation leaves it unhandled.

JavaScript options · Typeahead

Live · share menu
import { createTypeahead, keyRove } from '@mixedrays/keyrove';

const config = {
  items: '[role="menuitem"]',
  loop: true,
  rovingTabindex: true,
};
const typeahead = createTypeahead(config);
const menu = document.querySelector('#share');

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

Type E to focus Email a copy. Arrows skip the disabled item and wrap at the ends.

06 / Your application

Build more involved interfaces

Give an item a shortcut. Nest groups with their own keys. Or combine focus navigation with your application's behavior.

The tree is the fullest example: keyrove navigates visible rows and typeahead finds them by name. Your code opens and closes folders, sets ARIA states, and decides what activating a file does.

Start with the part you need; the same handler stays underneath.

Live · drawing tools
<div
  id="tools"
  role="toolbar"
  aria-orientation="vertical"
  aria-label="Drawing tools"
>
  <button
    data-keyrove-item
    data-keyrove-roving-tabindex
    data-keyrove-focus-key="KeyV"
    aria-keyshortcuts="V"
    tabindex="0"
  >
    Move
  </button>
  <button
    data-keyrove-item
    data-keyrove-roving-tabindex
    data-keyrove-focus-key="KeyR"
    aria-keyshortcuts="R"
    tabindex="-1"
  >
    Rectangle
  </button>
  <!-- … -->
  <button
    data-keyrove-item
    data-keyrove-roving-tabindex
    data-keyrove-focus-key="KeyT"
    aria-keyshortcuts="T"
    tabindex="-1"
  >
    Text
  </button>
</div>

Press P for Pen, then ↓ to continue.

Focus keys

What it leaves to you

keyrove moves focus and nothing else, so it fits whatever widget you are building.

keyrove does

  • Finds the next item and focuses it
  • Prevents the browser's default only for keys it handles
  • Moves the tab stop with focus when roving tabindex is on

You do

  • Roles, labels and ARIA states
  • Selection, and what Enter and Space do
  • Focusable items and the first tab stop

The listbox example puts all three together.

Start with a list

Install the package, mark the items, and pass keydown to keyRove.

Get started npm i @mixedrays/keyrove

Installation

Setup for vanilla JavaScript, React, Vue and Svelte.

Examples

Live demos, from a basic list to a tree view.

API reference

Every handler, option, key binding and type.

AI prompts

Point a coding assistant at these docs.