Skip to content

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 have tabindex="-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 asatomRovingFocusItem

Inputs

InputTypeDefaultDescription
atomRovingFocusItemDisabledbooleanWhether this item is disabled and should be skipped during navigation.

Outputs

OutputTypeDescription
atomRovingFocusItemDisabledChangebooleanWhether this item is disabled and should be skipped during navigation.

Properties

PropertyTypeDescription
tabindexSignal<0 | -1> (readonly)The computed tabindex: 0 when active, -1 otherwise.

AtomRovingFocusGroup

Selector[atomRovingFocusGroup]
Exported asatomRovingFocusGroup

Inputs

InputTypeDefaultDescription
atomRovingFocusGroupOrientationRovingOrientationThe navigation orientation. Defaults to 'vertical'.
atomRovingFocusGroupWrapbooleanWhether navigation wraps from last to first and vice versa.
atomRovingFocusGroupHomeEndbooleanWhether Home/End keys navigate to first/last items.
atomRovingFocusGroupDisabledbooleanDisables all keyboard navigation.

Outputs

OutputTypeDescription
atomRovingFocusGroupOrientationChangeRovingOrientationThe navigation orientation. Defaults to 'vertical'.
atomRovingFocusGroupWrapChangebooleanWhether navigation wraps from last to first and vice versa.
atomRovingFocusGroupHomeEndChangebooleanWhether Home/End keys navigate to first/last items.
atomRovingFocusGroupDisabledChangebooleanDisables all keyboard navigation.

Properties

PropertyTypeDescription
activeIdSignal<string | null> (readonly)The id of the currently active (tabbable) item.

Released under the MIT License.