Basic list

The default behaviour — arrows step one item, Home and End jump to the ends, PageUp and PageDown move in blocks, and Tab still does what Tab does.

A list needs two things: data-keyrove-item on every navigable element, and a keydown listener on the container. Tab to an item or click it, then use , Home / End, and PageUp / PageDown.

Both ways in work because both are still available: tabindex="0" makes each item a real tab stop, and keyrove adds movement on top without taking anything away. Tab keeps walking the list item by item here — see roving tabindex for making the whole group one stop instead.

Waiting for a keypress…
<ul id="menu" data-keyrove-page-length="5">
  <li data-keyrove-item tabindex="0">Item 1</li>
  <li data-keyrove-item tabindex="0">Item 2</li>
  <li data-keyrove-item tabindex="0">Item 3</li>
  <!-- … -->
  <li data-keyrove-item tabindex="0">Item 12</li>
</ul>
import { keyRove } from '@mixedrays/keyrove';

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

The arrows are a default

and are what a group answers to when you have not said otherwise. Two attributes on the root swap them for anything else:

<ul id="menu" data-keyrove-next-key="KeyJ" data-keyrove-prev-key="KeyK">

</ul>

The listener does not change, and the keys you did not bind — the arrows now included — go back to their browser behaviour. See custom keys.

Page length

data-keyrove-page-length sets how far PageUp and PageDown move. It defaults to 10; the demo above uses 5 so the jump is visible in a short list.

A page jump that would land past the end clamps to the last navigable item rather than doing nothing, so PageDown always makes progress until focus reaches the end.

Reacting to movement

The optional second argument takes onMove, fired after focus has moved, and only when it actually moved.

list.addEventListener('keydown', (e) => {
  keyRove(e, {
    onMove: ({ action, from, to }) => console.log(action, '→', to),
  });
});

The log under each demo on this site is wired up exactly that way. keyRove also returns what it didnull for an untouched key, the move for a consumed one — so handlers sharing a listener can chain with keyRove(e) || myOwnHandler(e).