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.
<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 did — null for an untouched
key, the move for a consumed one — so handlers sharing a listener can chain
with keyRove(e) || myOwnHandler(e).