Several components — lists, menus, tables, and grouped controls — share the same keyboard model rather than each inventing their own: the group is a single Tab stop, and once focus is inside it, arrow keys move between items. This is the model the WAI-ARIA authoring practices call for grids, listboxes, menus, and radio groups, and it's what makes Tab predictable regardless of how many items a list contains.
SegmentedControl) use
Left/Right; vertical groups (e.g. Table, Listbox) use Up/Down.rovingFocus. Listbox implements the same Home/End behavior itself via a separate,
hand-rolled key handler rather than through ListManager, and additionally treats
PageUp/PageDown the same as Home/End.rovingFocus-based horizontal groups swap Left/Right so "next"
and "previous" still match the direction the user reads in. Vertical navigation is
unaffected.How arrow-key navigation relates to selection differs by component, matching how each one is actually used:
SegmentedControl uses this.Enter/Space, or a click). This is the right model when moving focus is cheap but
activating is not — Table's row navigation uses this, since arrowing across rows
shouldn't fire a selection on every keystroke.Frontile has two building blocks that implement this model, at different levels of functionality:
rovingFocus (packages/frontile/src/utils/roving-focus.ts) is the minimal version:
it owns only keyboard handling and tabindex bookkeeping, reading selected/disabled state
directly off each element's attributes (data-selected, aria-checked, aria-selected,
:disabled, aria-disabled). It renders nothing and knows nothing about selection state
itself. SegmentedControl and Table use it directly — see the
rovingFocus reference
for the full
API.ListManager (packages/frontile/src/utils/listManager.ts) is the fuller-featured
engine behind Listbox, Dropdown, Select, Autocomplete, NativeSelect, and Command.
It additionally owns selection state (single/multiple/none), typeahead search, and
auto-activation of the first or previously-selected item when a list opens.You won't normally interact with either primitive directly unless you're building a new list-like component — as a consumer, what matters is that the keyboard behavior above is consistent everywhere you see it.