Roving Focus
Manage arrow-key navigation and roving tabindex across a group of focusable items.
Why use these atoms?
- Implements the WAI-ARIA roving tabindex pattern — only the active item has
tabindex="0", all others havetabindex="-1" - Arrow key navigation with configurable orientation (vertical or horizontal)
- Optional wrapping from last to first and vice versa
- Home/End key support to jump to first/last items
- Automatically skips disabled items during navigation
- Items self-register and unregister — no manual list management
- DOM order sorting via
compareDocumentPosition— items don't need to be in a specific order in the template - Foundation for tabs, menus, radio groups, toolbars, listboxes, and tree views
Import
ts
import {AtomRovingFocusGroup, AtomRovingFocusItem} from '@terse-ui/atoms/roving-focus';Usage
html
<ul atomRovingFocusGroup>
<li atomRovingFocusItem>One</li>
<li atomRovingFocusItem>Two</li>
<li atomRovingFocusItem>Three</li>
</ul>The first item gets tabindex="0". When the user presses ArrowDown, focus moves to the next item — the previous item gets tabindex="-1" and the new item gets tabindex="0".
Horizontal Navigation
html
<div atomRovingFocusGroup atomRovingFocusGroupOrientation="horizontal" role="tablist">
<button atomRovingFocusItem role="tab">Tab 1</button>
<button atomRovingFocusItem role="tab">Tab 2</button>
<button atomRovingFocusItem role="tab">Tab 3</button>
</div>In horizontal mode, ArrowLeft/ArrowRight navigate instead of ArrowUp/ArrowDown.
Disabled Items
html
<ul atomRovingFocusGroup>
<li atomRovingFocusItem>Enabled</li>
<li atomRovingFocusItem [atomRovingFocusItemDisabled]="true">Disabled (skipped)</li>
<li atomRovingFocusItem>Enabled</li>
</ul>Disabled items are skipped during keyboard navigation and cannot be activated by click.
As Host Directives
ts
@Directive({
selector: '[myTabList]',
hostDirectives: [{
directive: AtomRovingFocusGroup,
inputs: ['atomRovingFocusGroupOrientation'],
}],
})
export class MyTabList {}
@Directive({
selector: '[myTab]',
hostDirectives: [AtomRovingFocusItem],
})
export class MyTab {}Keyboard Interactions
- ArrowDown / ArrowUp: Navigate in vertical mode
- ArrowRight / ArrowLeft: Navigate in horizontal mode
- Home: Focus first item
- End: Focus last item
API Reference
AtomRovingFocusItem
| Selector | [atomRovingFocusItem] |
| Exported as | atomRovingFocusItem |
Inputs
| Input | Type | Default | Description |
|---|---|---|---|
atomRovingFocusItemDisabled | boolean | Whether this item is disabled and should be skipped during navigation. |
Outputs
| Output | Type | Description |
|---|---|---|
atomRovingFocusItemDisabledChange | boolean | Whether this item is disabled and should be skipped during navigation. |
Properties
| Property | Type | Description |
|---|---|---|
tabindex | Signal<0 | -1> (readonly) | The computed tabindex: 0 when active, -1 otherwise. |
AtomRovingFocusGroup
| Selector | [atomRovingFocusGroup] |
| Exported as | atomRovingFocusGroup |
Inputs
| Input | Type | Default | Description |
|---|---|---|---|
atomRovingFocusGroupOrientation | RovingOrientation | The navigation orientation. Defaults to 'vertical'. | |
atomRovingFocusGroupWrap | boolean | Whether navigation wraps from last to first and vice versa. | |
atomRovingFocusGroupHomeEnd | boolean | Whether Home/End keys navigate to first/last items. | |
atomRovingFocusGroupDisabled | boolean | Disables all keyboard navigation. |
Outputs
| Output | Type | Description |
|---|---|---|
atomRovingFocusGroupOrientationChange | RovingOrientation | The navigation orientation. Defaults to 'vertical'. |
atomRovingFocusGroupWrapChange | boolean | Whether navigation wraps from last to first and vice versa. |
atomRovingFocusGroupHomeEndChange | boolean | Whether Home/End keys navigate to first/last items. |
atomRovingFocusGroupDisabledChange | boolean | Disables all keyboard navigation. |
Properties
| Property | Type | Description |
|---|---|---|
activeId | Signal<string | null> (readonly) | The id of the currently active (tabbable) item. |