Looping lists
Make keyboard navigation wrap from the last item to the first and back again, and decide when a list should loop and when it should stop at its ends.
By default, navigation stops at the first and last items. Add
data-keyrove-loop to the container to wrap between them:
- ↓ from Sign out moves to Profile.
- ↑ from Profile moves to Sign out.
You can sign out any time you like, but the arrows will never leave. Tab will.
<ul id="account-menu" data-keyrove-loop>
<li data-keyrove-item tabindex="0">Profile</li>
<li data-keyrove-item tabindex="0">Settings</li>
<li data-keyrove-item tabindex="0">Keyboard shortcuts</li>
<li data-keyrove-item tabindex="0">Help</li>
<li data-keyrove-item tabindex="0">Sign out</li>
</ul>
import { keyRove } from '@mixedrays/keyrove';
const menu = document.querySelector<HTMLElement>('#account-menu')!;
menu.addEventListener('keydown', (e) => keyRove(e));
keyrove reads the attribute on every keypress. You can turn looping on or off without changing the listener or reinitializing the group.
Boolean value
Both data-keyrove-loop and data-keyrove-loop="true" enable looping.
Set data-keyrove-loop="false" to disable it. The same rule applies to all
boolean keyrove attributes, including data-keyrove-item, data-keyrove-root,
data-keyrove-skip, and data-keyrove-roving-tabindex.
In JSX, pass a boolean directly:
<ul data-keyrove-loop={loop}>…</ul>
Entering at either end
When no item is focused and the group's listener receives a directional key,
keyrove focuses the first navigable item. In a looping list, prev enters at
the last navigable item instead. With the default bindings:
- ↓ enters at the first item.
- ↑ enters at the last item.
This matches the optional arrow-key entry behavior in the APG menu button pattern.
Home, End, and the page keys still require focus inside an item.
Only next and prev wrap
Looping changes only next and prev:
- Home and End move to the first and last navigable items.
- PageUp and PageDown stop at either end if a jump would go past it.
If you rebind next and prev to J and
K, those keys wrap and the arrows return to their browser
defaults. See
custom keys.
Skipped items keep their place
A wrap lands on the first or last navigable item, passing over items marked
data-keyrove-skip or disabled. See skipped items.
Nowhere to go
If the only navigable item already has focus, next and prev leave it there.
keyrove still calls preventDefault(), but onMove does not fire.
For next, the return value is
{ action: 'next', from, to: null }, just as at the end of a non-looping list.
Grids keep their edges
Looping applies only to lists. When data-keyrove-cols is greater than 1,
the group is a grid and ignores data-keyrove-loop.
Cell moves continue across row boundaries, but stop at the first or last cell;
row moves stop at the top or bottom. At these edges, keyrove prevents the
browser's default action without moving focus. See the
APG grid pattern for keyboard
navigation guidance.
When a list should wrap
Wrapping works well for short menus, pickers, and tab lists where both ends are visible.
For long, scrolling lists, keeping the ends helps users track their position. Wrapping can unexpectedly jump the viewport back to the top and may be hard to notice with a screen reader. Without looping, holding ↓ at the bottom keeps focus there and prevents the page from scrolling.