Custom keys

Arrows are the default, not the rule — rebinding the next and previous keys to any key combo.

Nothing about keyrove is tied to the arrow keys. The keys that move focus are attributes on the root, and ArrowDown / ArrowUp are simply what they fall back to. data-keyrove-next-key and data-keyrove-prev-key rebind them to any KeyboardEvent.code, with or without modifiers.

A toolbar, for one, navigates left-to-right rather than up-and-down. move between the buttons here; the up and down arrows are left to scroll the page.

Waiting for a keypress…
<div
  id="toolbar"
  data-keyrove-next-key="ArrowRight"
  data-keyrove-prev-key="ArrowLeft"
>
  <button data-keyrove-item tabindex="0">Bold</button>
  <button data-keyrove-item tabindex="0">Italic</button>
  <button data-keyrove-item tabindex="0">Underline</button>
  <!-- … -->
  <button data-keyrove-item tabindex="0">Code</button>
</div>
<div data-keyrove-next-key="ArrowRight" data-keyrove-prev-key="ArrowLeft">

</div>

Rebinding is a markup change and nothing else — the keyRove(e) call is identical whatever the group answers to.

For exactly this pair there is a more idiomatic spelling: data-keyrove-orientation="horizontal" maps the defaults to ArrowRight/ArrowLeft and flips them under RTL, so the "forward" arrow follows the text direction instead of being hardcoded. Reach for the explicit key attributes when the keys are anything other than the reading-direction arrows — they win over orientation wherever both are set.

Any key, not a shortlist

The attribute value is a combo — an optional set of modifiers and a KeyboardEvent.code — matched against the event on every press, so there is no set of supported keys to choose from:

<!-- vim-style, for a results list in a keyboard-first app -->
<ul data-keyrove-next-key="KeyJ" data-keyrove-prev-key="KeyK">

</ul>

<!-- game-style -->
<ul data-keyrove-next-key="KeyS" data-keyrove-prev-key="KeyW">

</ul>

<!-- the number pad, for a kiosk with no arrow cluster -->
<ul data-keyrove-next-key="Numpad2" data-keyrove-prev-key="Numpad8">

</ul>

Each root is read on its own, so two groups on the same page can answer to completely different keys with one delegated listener serving both.

Modifiers

Prefix the code with any of mod+, ctrl+, alt+, shift+, meta+ — in any order and any case. mod resolves to meta on Apple platforms and ctrl elsewhere:

<ul
  data-keyrove-next-key="ctrl+ArrowRight"
  data-keyrove-prev-key="ctrl+ArrowLeft"
>

</ul>

Matching is exact in both directions: a declared modifier is required, an undeclared one is forbidden. A bare KeyJ binding means "J with nothing else held", so Ctrl+J keeps its browser default — and a ctrl+KeyJ binding never fires on a plain J.

Codes, not keys

These are KeyboardEvent.code values — physical keys — not KeyboardEvent.key values. KeyW rather than w, and the code does not change with the keyboard layout, so a binding chosen for QWERTY lands on the same physical key on AZERTY.

That cuts both ways, and it is the thing to weigh when picking a letter: a binding is to a position on the keyboard. KeyJ and KeyK sit under the right hand on QWERTY, which is the whole point of the vim bindings; on Dvorak those same positions are c and t.

Everything else keeps its default

Only bound keys are acted on, and preventDefault() is called only for those. With KeyJ and KeyK above, the arrow keys stop moving focus entirely and go back to scrolling the page.

Tab and Shift+Tab are never bound by default, which is what lets custom keys sit alongside native tab navigation rather than in place of it.

Two things that are not rebindable

Home, End, PageUp and PageDown are fixed — there is no attribute for them. So is the grid's cell movement, below.

Interaction with grids

In a grid, and already move one cell. Binding one of them as the next or previous key hands it to that binding instead — the cell move gives way, so the two never both fire on one keypress.

Binding a non-arrow pair leaves cell movement alone: with KeyJ and KeyK on a grid root, J and K move a whole row while still move a cell.

Editable elements are exempt

A key pressed inside an input, textarea, select, or [contenteditable] element is never handled, whatever it is bound to. Arrows and Home/End keep moving the caret, and a letter binding like KeyJ does not swallow typing "j" into a field that sits inside an item — navigation resumes once focus leaves the field.

A binding worth avoiding

Tab itself. Binding data-keyrove-next-key="Tab" does work, and it costs you the browser's own forward tab navigation for the whole group. Shift+Tab is a different combo, so it stays native and remains the way out — but a group where the two Tab directions behave that differently is confusing. If what you want is Tab moving past a group rather than through it, roving tabindex is the mechanism for that, and it leaves the key alone.