Roving Tabindex
stimeo--roving
Makes a set of items a single Tab stop, navigated with the arrow keys.
The stimeo--roving controller is the APG roving-tabindex technique as a standalone controller — the policy layer over the shared RovingTabindex util (the counterpart to Focus Scope over FocusTrap). Exactly one item is tabindex=0 (the rest -1); the arrow keys move focus and that tab stop together — ArrowRight/ArrowLeft when horizontal, ArrowUp/ArrowDown when vertical, both axes when both — and Home/End jump to the ends. With wrap=true movement cycles past the ends; with wrap=false it clamps. Listeners are delegated on the container (keydown for movement, focusin to sync the tab stop when focus arrives by click or programmatically), so items added or removed at runtime need no per-item wiring. It emits change when the tabbable item changes. Behavior only: it owns tabindex and focus movement, not roles, selection, typeahead, or activation — those stay with the consuming pattern (here, plain action buttons).
Tab into the group once, then use the arrow keys to move between buttons; Home/End jump to the ends.
Keyboard
| Key | Action |
|---|---|
| → / ← | Move to the next / previous item (horizontal or both orientation). |
| ↓ / ↑ | Move to the next / previous item (vertical or both orientation). |
| Home / End | Move to the first / last item (when homeEnd is on). |
<%# Roving tabindex demo: the toolbar is a single Tab stop. Tab in once, then the
arrow keys move focus (and the tab stop) between buttons; Home/End jump to the ends
and movement wraps (the defaults). role="toolbar" + the accessible name are the
consumer's — the library owns only tabindex + focus movement, so this is "build your
own toolbar with the roving primitive". The buttons are plain actions; demo.css owns
the look. %>
<div class="roving-demo">
<p class="roving-demo__hint"><%= t("components.roving.demo.hint") %></p>
<div class="roving-demo__group" role="toolbar"
aria-label="<%= t('components.roving.demo.group_label') %>"
data-controller="stimeo--roving">
<button type="button" class="demo-trigger" data-stimeo--roving-target="item">
<%= t("components.roving.demo.items.previous") %>
</button>
<button type="button" class="demo-trigger" data-stimeo--roving-target="item">
<%= t("components.roving.demo.items.play") %>
</button>
<button type="button" class="demo-trigger" data-stimeo--roving-target="item">
<%= t("components.roving.demo.items.next") %>
</button>
<button type="button" class="demo-trigger" data-stimeo--roving-target="item">
<%= t("components.roving.demo.items.shuffle") %>
</button>
<button type="button" class="demo-trigger" data-stimeo--roving-target="item">
<%= t("components.roving.demo.items.repeat") %>
</button>
</div>
</div>
/*
* Presentation-only styles for the roving-tabindex demo. The library owns the single
* Tab stop (it sets tabindex) and focus movement; this CSS only lays out the toolbar
* row. The focused button is highlighted by the shared .demo-trigger :focus-visible
* outline, so the roving focus is visible without any extra rule here.
*/
.roving-demo {
display: flex;
flex-direction: column;
gap: 0.75rem;
align-items: flex-start;
}
.roving-demo__hint {
margin: 0;
color: var(--muted);
}
.roving-demo__group {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
This demo needs no consumer-side JS (the controller handles the behavior).
These demo styles use shared design tokens (light + dark). Copy the shared styles too, then toggle data-theme on your root element for dark mode.
The data-* attributes you add to your own HTML to wire this component. Put the data-controller below on a root element, then place its targets / values / actions inside that element.
On the root element
data-controller="stimeo--roving"
Targets
| Name | Description | Attribute |
|---|---|---|
item
required
|
A navigable item; the items form one Tab stop with arrow-key roving among them. | data-stimeo--roving-target="item" |
Values
| Name | Description | Attribute |
|---|---|---|
orientation
|
Arrow-key axis: horizontal, vertical, or both; default horizontal. |
data-stimeo--roving-orientation-value |
wrap
|
Whether arrow movement cycles past the ends, else clamps; default true. | data-stimeo--roving-wrap-value |
homeEnd
|
Whether Home/End jump to the first/last item; default true. | data-stimeo--roving-home-end-value |
Events
| Name | Description | Event |
|---|---|---|
change
|
Fires with { index, item } when the tabbable item changes. | stimeo--roving:change |
State hooks
The library only manages these ARIA/data attributes and custom properties. Your CSS reads them to render the look — selectors like [aria-selected], [aria-expanded], or var(--stimeo--…) hook into this state.
| Hook | Target | Meaning |
|---|---|---|
tabindex |
Item | 0 on the tabbable item, -1 on the rest (single Tab stop). |