Roving Tabindex
stimeo--roving
Turns a row of items into a single Tab stop, moved through with the arrow keys.
Makes a group of buttons or tabs stop the Tab key just once, with the arrow keys moving inside it. A group of twenty items becomes one stop instead of twenty. It handles left and right for a row, up and down for a column, or both. Home and End jump to the ends. At an end you choose whether it wraps around or stops. When focus arrives by a click or from your own code, the Tab stop follows it. Items added later need no setup of their own.
Tab into the group once, then move between the buttons with the arrow keys. Home and End jump to the ends.
Keyboard
| Key | Action |
|---|---|
| → / ← | Move to the next / previous item (horizontal or both orientation). Under RTL the two swap. |
| ↓ / ↑ | Move to the next / previous item (vertical or both orientation). |
| Home / End | Move to the first / last reachable item (when homeEnd is on). |
| Ctrl+Home / Ctrl+End and the like | A modified Home/End is left to the browser, not consumed. |
<%# 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"
data-stimeo--roving-orientation-value="both">
<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 a user action moves the tabbable item; establishing and re-establishing are silent. | 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). |