Grid
Declaring a column count switches Up and Down to whole-row moves and brings Left and Right into play.
Add data-keyrove-cols-length to the root and the same list navigates as a
grid: ↑ ↓ move a whole row, so
focus lands on the item directly above or below, and ←
→ move one cell.
Focus stops at the edges instead of wrapping — arrowing left from the first column stays put rather than jumping to the end of the row above.
The row moves follow whatever the root's next and previous keys are, so a grid
bound to KeyJ / KeyK moves a row on those and still moves a cell on
← →. Bind one of the horizontal
arrows instead and it takes over the row move, giving up the cell move — see
custom keys.
<div id="grid" data-keyrove-cols-length="6" data-keyrove-page-length="2">
<button data-keyrove-item tabindex="0">1</button>
<button data-keyrove-item tabindex="0">2</button>
<button data-keyrove-item tabindex="0">3</button>
<!-- … -->
<button data-keyrove-item tabindex="0">18</button>
</div>
document.querySelector('#grid').addEventListener('keydown', (e) => keyRove(e));
The column count is the one keyrove navigates by. Keep it in step with however
the grid is laid out — a CSS grid-template-columns of six and a
data-keyrove-cols-length of four will move focus in a way that does not match
what is on screen.
Paging in a grid
data-keyrove-page-length counts rows once a column count is set, so
PageDown moves the same number of rows regardless of how
wide the grid is — and focus keeps the column it started in. The demo above uses
2, so a page is two rows of six.
Responsive grids
The column count is read from the attribute on every keypress, not cached, so updating it when a breakpoint changes is enough to keep navigation matching the layout.
const media = window.matchMedia('(min-width: 768px)');
const syncColumns = () => {
grid.setAttribute('data-keyrove-cols-length', media.matches ? '6' : '3');
};
media.addEventListener('change', syncColumns);
syncColumns();