# API reference > Every export in the package — the handler, the helper, the key bindings, the attributes, and the types. ## keyRove(event, options?) Handles keyboard navigation within the event's navigation root, moving focus to the element the pressed key resolves to. ```ts import { keyRove } from '@mixedrays/keyrove'; list.addEventListener('keydown', (e) => keyRove(e)); ``` The root is the nearest ancestor of the event target carrying `data-keyrove-root`, falling back to the element the listener is attached to (`currentTarget`). That fallback is what lets one delegated listener serve several independent groups — see [several groups, one listener](/docs/installation#several-groups-one-listener). Because it is the _nearest_ ancestor, roots can also be [nested](/docs/examples/nested-roots): the innermost one governs while focus is inside it. `preventDefault()` is called for every key keyrove acts on, so the page does not scroll while you move through a list. "Acts on" means there was a position to move from or to: at the end of a list or the edge of a grid the key is still consumed — a group owns its bound keys up to its own boundary — but on a group where no item holds focus, or one with no items at all, a bound key keeps its browser default rather than being swallowed. Keys it does not act on are left entirely untouched — Tab, Shift+Tab, Enter, Space and Escape among them, which is why keyrove composes with native focus navigation instead of replacing it. Keys arriving from an editable target — a `textarea`, a `select`, a `[contenteditable]` element (descendants included; a `contenteditable="false"` island opts back out), or an `input` whose keys act natively (text entry, `number`, `range`, `radio`, …) — are never acted on: the caret or value keeps the arrows and Home/End, and typing into a field inside an item is not swallowed by a printable-key binding. Inputs where the bound keys are inert — a `checkbox`, a `button` — still navigate, so a list of checkbox rows keeps its arrows. ### Keys The forward and back keys are configurable; the rest are fixed. | Key | Bound by | In a list | In a grid | | ----------------------- | ----------------------- | --------------------------- | -------------------------- | | `ArrowDown` _(default)_ | `data-keyrove-next-key` | Next item | Next row, same column | | `ArrowUp` _(default)_ | `data-keyrove-prev-key` | Previous item | Previous row, same column | | `ArrowRight` | fixed | — | Next cell in the row | | `ArrowLeft` | fixed | — | Previous cell in the row | | `Home` | fixed | First navigable item | First navigable item | | `End` | fixed | Last navigable item | Last navigable item | | `PageDown` | fixed | Forward `page-length` items | Forward `page-length` rows | | `PageUp` | fixed | Back `page-length` items | Back `page-length` rows | The first two rows show what a group answers to with no configuration. Set `data-keyrove-next-key` or `data-keyrove-prev-key` on the root and that key takes over the row, while the default it replaced goes back to its browser behaviour — see [custom keys](/docs/examples/custom-keys). The value is a combo: zero or more of `mod+` / `ctrl+` / `alt+` / `shift+` / `meta+` (any order, any case) followed by a [`KeyboardEvent.code`](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code). `mod` resolves to `meta` on Apple platforms and `ctrl` elsewhere. Matching is exact — declared modifiers are required, undeclared ones are forbidden — so `KeyJ` matches only while Ctrl is _not_ held, and shortcuts like Ctrl+ArrowDown keep their browser defaults inside a group bound to the bare arrows. The fixed keys are bare combos too: a modified Home or PageDown is left alone. `data-keyrove-orientation="horizontal"` re-points the two defaults at `ArrowRight`/`ArrowLeft` without spelling the combos out — and flips the pair under RTL, resolved from the nearest `dir` attribute and falling back to the computed direction, so a toolbar reads "forward" the way its text does. It only supplies defaults: an explicit `data-keyrove-next-key` or `data-keyrove-prev-key` wins over it, and the freed vertical arrows go back to their browser behaviour. A grid ignores the attribute — its cell moves already cover the horizontal axis. At the ends of a list, next and prev are consumed without moving — see the [return value](#return-value). With `data-keyrove-loop` on the root they wrap instead: forward from the last navigable item lands on the first, and back from the first lands on the last. A looping group is also entered as a circle — the prev key pressed from outside lands on the _last_ item, matching the APG menu-button convention. Wrapping is for lists only; a grid keeps its edges, per the APG grid pattern. The two `Arrow` cell moves in a grid stand down when the press already matched the next or previous binding, so one keypress never fires both. `Home`, `End`, `PageUp`, and `PageDown` only act once focus is genuinely inside an item: they move _within_ a group rather than into one. The next and previous keys differ on purpose — pressed on a group where nothing is focused yet, they move focus to the first item (the last, for the prev key on a looping group), which is how you enter a list from the keyboard. An item counts as focused when focus is anywhere inside it (`:focus-within`), so an item that wraps a link or a button is still the navigation position after Tab lands on that inner control. ### options.onMove Fired _after_ focus has moved, and only when it actually moved: a consumed key with nowhere to go — the end of a list, the edge of a grid — fires nothing. ```ts keyRove(e, { onMove: ({ action, from, to }) => console.log(action, from, to), }); ``` `action` names the move (`'next'`, `'prev'`, `'home'`, `'end'`, `'pageUp'`, `'pageDown'`); `from` is the item focus left — `null` when an arrow entered the group from outside — and `to` the item it landed on. In a grid, a left or right cell move reports as `prev` and `next`. ### Return value `keyRove` reports what it did with the key, so handlers compose: - `null` — the key was not keyrove's and is untouched, browser default included. - `{ action, from, to }` — the key was consumed. `to` is the newly focused item, or `null` for a consumed no-op: a bound key pressed at an edge, where the group owns the key but there is nowhere left to go. A non-null result means "claimed", which is what lets several handlers share one listener without stepping on each other: ```ts element.addEventListener('keydown', (e) => keyRove(e) || myOwnHandler(e)); ``` One keypress resolves to at most one action. A custom binding that collides with a fixed key — say `data-keyrove-next-key="Home"` — takes the press, and the fixed key stands down. ## createTypeahead(options?) Builds a keydown handler that focuses items as their labels are typed — printable characters accumulate in a buffer, and focus jumps to the first navigable item whose label starts with it, case-insensitively. ```ts import { keyRove, createTypeahead } from '@mixedrays/keyrove'; const typeahead = createTypeahead(); list.addEventListener('keydown', (e) => keyRove(e) || typeahead(e)); ``` A factory rather than a plain handler on purpose: a buffer is state, and holding it in the returned closure keeps `keyRove` itself stateless. Create one handler per listener; the buffer resets after `options.resetMs` (default `500`) milliseconds of typing silence, with no timer to clean up. The label is the item's `data-keyrove-typeahead` attribute, falling back to its trimmed `textContent` when the attribute is absent or empty. Items carrying `data-keyrove-skip` or `disabled` are passed over, and the root resolves exactly as in `keyRove` — nearest `data-keyrove-root`, else `currentTarget` — so the same delegated listener serves both. Unlike bindings, which match the physical `e.code`, typeahead reads `e.key` — the produced character, which is what the pressed key means in the user's keyboard layout. A press is buffered only when it is genuinely typing: single characters without Ctrl/Alt/Meta (Shift stays — it is how capitals are typed), never inside an [editable element](/docs/examples/custom-keys#editable-elements-are-exempt), and a space only once a match is underway, so the spacebar keeps scrolling and clicking. A character that matches nothing is left to its browser default. The handler follows the `keyRove` contract: `null` when the key was left untouched, `{ action: 'typeahead', from, to }` when it was consumed — with `to: null` when the buffer grew but still names the already-focused item. `options.onMove` fires after focus has moved and only when it actually moved, mirroring [`keyRove`'s option](#options-onmove), so both handlers can feed the same follow-focus logic. Chain the handler _after_ navigation, as above, so a printable binding like `KeyJ` navigates instead of entering the buffer. The roving tab stop moves with the match, exactly as for an arrow move. ## matchesCombo(event, combo) Whether a keydown event matches a combo — the matcher behind every key check keyrove itself makes, exported for your own handlers. ```ts import { matchesCombo } from '@mixedrays/keyrove'; list.addEventListener('keydown', (e) => { if (matchesCombo(e, 'Escape')) closePanel(); if (matchesCombo(e, 'mod+KeyK')) openPalette(); }); ``` A combo is zero or more of `mod+` / `ctrl+` / `alt+` / `shift+` / `meta+` (any order, any case) followed by a `KeyboardEvent.code`; whitespace around the parts is ignored. `mod` resolves to `meta` on Apple platforms and `ctrl` elsewhere. Matching is exact: every declared modifier must be held and every undeclared one must not be, so `'Escape'` above rejects Ctrl+Escape. The code part is matched case-sensitively against `e.code` — the physical key, unaffected by keyboard layout. No `code` value contains a `+` (the plus key itself is `Equal` or `NumpadAdd`), so the separator is unambiguous; a combo that names an unknown modifier or ends in a dangling `+` matches nothing. ## toggleTabIndex({ root, isActive }) Sets `tabindex` to `0` or `-1` on a single element. ```ts import { toggleTabIndex } from '@mixedrays/keyrove'; toggleTabIndex({ root: firstItem, isActive: true }); ``` Exported for the cases where you manage the tab stop yourself — establishing the first one in a [roving group](/docs/examples/roving-tabindex), or restoring it after re-rendering a list. Descendant tab stops are deliberately left alone; roving tabindex only needs the item itself to carry the stop. A nullish `root` is a no-op, so a query that found nothing does not need guarding at the call site. ## Attributes | Attribute | On | Default | Meaning | | ------------------------------ | ---- | ----------- | ---------------------------------------------------------------------------------------- | | `data-keyrove-item` | item | — | Marks an element as navigable. | | `data-keyrove-skip` | item | — | Passed over when moving; stays in the DOM order. | | `data-keyrove-roving-tabindex` | item | — | Moves the `tabindex="0"` tab stop with focus. | | `data-keyrove-root` | root | — | Marks the navigation root explicitly, instead of using the listener's element. | | `data-keyrove-cols-length` | root | `1` | A value above 1 switches the group to grid navigation. | | `data-keyrove-page-length` | root | `10` | Items per page jump — whole rows in a grid. | | `data-keyrove-next-key` | root | `ArrowDown` | Combo that moves forward, e.g. `KeyJ` or `ctrl+ArrowRight`. | | `data-keyrove-prev-key` | root | `ArrowUp` | Combo that moves back. | | `data-keyrove-loop` | root | — | Next/prev wrap past the ends of a list. Grids never wrap. | | `data-keyrove-orientation` | root | — | `horizontal` maps the default keys to `ArrowRight`/`ArrowLeft`, RTL-aware. | | `data-keyrove-typeahead` | item | text | Label for [type-to-focus](#createtypeahead-options), when the item's own text is not it. | Root attributes are read on every keypress rather than cached, so changing one takes effect immediately — see [responsive grids](/docs/examples/grid#responsive-grids). Elements carrying `disabled` are excluded from navigation without needing `data-keyrove-skip`. ### Constants Each attribute name is also exported, so markup built in JavaScript need not hardcode strings: ```ts import { KEYROVE_ATTR_ITEM, KEYROVE_ATTR_SKIP, KEYROVE_ATTR_ROOT, KEYROVE_ATTR_NEXT_KEY, KEYROVE_ATTR_PREV_KEY, KEYROVE_ATTR_PAGE_LENGTH, KEYROVE_ATTR_COLS_LENGTH, KEYROVE_ATTR_ROVING_TABINDEX, KEYROVE_ATTR_LOOP, KEYROVE_ATTR_ORIENTATION, KEYROVE_ATTR_TYPEAHEAD, } from '@mixedrays/keyrove'; ``` ## Types ```ts import type { KeyRoveCode, KeyRoveEvent, Move, MoveAction, MoveResult, Options, TypeaheadMove, TypeaheadOptions, TypeaheadResult, } from '@mixedrays/keyrove'; ``` ### KeyRoveEvent The shape keyrove needs from a keydown event — structural rather than a union of `KeyboardEvent | React.KeyboardEvent`, so the package stays dependency-free while accepting both. ```ts type KeyRoveEvent = { code: KeyRoveCode; target: EventTarget | null; currentTarget: EventTarget | null; preventDefault: () => void; ctrlKey?: boolean; altKey?: boolean; shiftKey?: boolean; metaKey?: boolean; key?: string; }; ``` The modifier flags are optional so a hand-built event object still qualifies, and a missing flag reads as "not held". Native and framework events carry all four; if you bridge events through an object of your own, forward them — an object without them matches every binding as though no modifier were pressed. `key` is optional for the same reason and only [typeahead](#createtypeahead-options) reads it: matching typed text needs the layout-dependent character, where bindings deliberately stay on the physical `code`. An event without it navigates as ever; it just never typeaheads. ### KeyRoveCode A `KeyboardEvent.code`. The union arm keeps it assignable from a plain `string` — which is how both the DOM and React type `code`, and what the code part of any `data-keyrove-*-key` combo is — while editors still complete the codes keyrove handles by default. It documents intent; it does not validate, and it does not constrain what you can bind. ```ts type KeyRoveCode = | 'ArrowUp' | 'ArrowDown' | 'ArrowLeft' | 'ArrowRight' | 'Home' | 'End' | 'PageUp' | 'PageDown' | (string & {}); ``` ### MoveAction, MoveResult, Move ```ts type MoveAction = 'home' | 'end' | 'next' | 'prev' | 'pageUp' | 'pageDown'; // what keyRove returns for a consumed keypress type MoveResult = { action: MoveAction; from: Element | null; to: Element | null; }; // what onMove receives: a move that actually happened type Move = MoveResult & { to: Element }; type Options = { onMove?: (move: Move) => void }; ``` ### TypeaheadOptions, TypeaheadResult, TypeaheadMove What [`createTypeahead`](#createtypeahead-options) takes and its handler returns. The result is `MoveResult`'s shape with its own action, which is what lets the two handlers share one listener with `||`. ```ts type TypeaheadOptions = { resetMs?: number; // buffer lifetime, default 500 onMove?: (move: TypeaheadMove) => void; }; type TypeaheadResult = { action: 'typeahead'; from: Element | null; to: Element | null; // null: the buffer grew but still names the focused item }; // what onMove receives: a move that actually happened type TypeaheadMove = TypeaheadResult & { to: Element }; ```