# rovingFocus One Tab stop for a group of items, arrow keys between them: toolbars, tabs, radio groups, menus, chips. The current item has `tabindex="0"` and every other item `tabindex="-1"`. The behavior owns `tabindex` on the items, so a template never binds it. ```ts Signature function rovingFocus(container: HTMLElement, opts: RovingOptions): Roving; type Orientation = "horizontal" | "vertical" | "both"; ``` ## Options | Option | Type | Default | What it does | |---|---|---|---| | `items` | `string` | required | Selector for the items, matched inside the container. | | `orientation` | `Orientation` | `"horizontal"` | Which arrows move. Left and Right flip under rtl. | | `loop` | `boolean` | `true` | Wrap from the last item to the first. | | `dir` | `"ltr" \| "rtl"` | the container's computed direction | Reading direction. | | `homeEnd` | `boolean` | `true` | Home and End jump to the first and last item. | | `page` | `number` | `0` (off) | PageUp and PageDown move this many items. | | `typeahead` | `boolean` | `false` | Letters jump to a matching item ([typeahead](typeahead)). | | `focusDisabled` | `boolean` | `false` | Disabled items stay focusable, as in a menu. | | `current` | `() => HTMLElement \| null` | the first item | The item that holds the tab stop when focus is outside: the selected tab. | | `onFocusItem` | `(el: HTMLElement) => void` | none | Called after an item receives focus by keyboard. Automatic activation goes here. | ## Keys | Keys | Action | |---|---| | `ArrowRight`, `ArrowLeft` | Next and previous item in a horizontal or both group; flipped under rtl. | | `ArrowDown`, `ArrowUp` | Next and previous item in a vertical or both group. | | `Home`, `End` | First and last item, with `homeEnd`. | | `PageDown`, `PageUp` | `page` items forward or back, stopping at the ends. | | Letters | The next item whose text starts with what was typed, with `typeahead`. | Disabled items (`disabled`, `aria-disabled="true"`, `data-disabled`) are skipped unless `focusDisabled`. Hidden items hold `-1`, so an item shown later by a class or style change joins without a second Tab stop, and a group started while hidden takes its stop when it is shown. Items added later are picked up on the next key press or focus; a stop that becomes disabled or hidden hands the stop to the next usable item. A text field or another arrow-key widget inside an item keeps its arrows, Home, End and letters. When the focused item is removed (a deleted chip, a closed tab), focus moves to the item that took its place, or the one before it, instead of falling to the body. ## The handle | Member | What it does | |---|---| | `items()` | The usable items in DOM order: visible and, unless `focusDisabled`, enabled. | | `focus(el)` | Moves the tab stop to el and focuses it. | | `sync()` | Re-applies tabindex after the items changed. Runs on its own on DOM changes. | | `setCurrent(el)` | Moves the tab stop to el without focusing it, when a selection changed from outside. | | `release()` | Stops and puts back the tabindex it changed. | ## In a template of your own ```ehtml app/shared/templates/chip-group/index.ehtml import { rovingFocus, type Roving } from "@elements/ui/behaviors"; interface ChipGroupState { roving?: Roving; }
ui.roving = rovingFocus(e.target as HTMLElement, { items: "[role=radio]", current: () => (e.target as HTMLElement).querySelector("[aria-checked=true]"), onFocusItem: (el) => value = el.dataset.value ?? "", })} onremove={() => ui.roving?.release()}>
``` Tab lands on the checked chip, arrows move between chips, and `onFocusItem` selects as focus moves. ## Writes `tabindex` on every item, put back on release. ## Used by [DataTable](../templates/data-table), [Kanban](../templates/kanban), [Menubar](../templates/menubar), [RichText](../templates/rich-text), [Tabs](../templates/tabs), [ToggleGroup](../templates/toggle-group).