The page navigation is complete. You may now navigate the page content as you wish.
Skip to main content

Composite

An internal utility component used to manage 1D and 2D keyboard navigation and roving focus for a collection of items.

This component is intended only for internal Helios use. If you need to use it, contact the Design Systems Team.

How to use this component

Composite is a headless component that manages 1D and 2D keyboard navigation and focus management for a collection of items. It associates a "composite" container element with nested "item" and optional "group" elements.

The component automatically routes focus between items based on your configured orientation and grouping. It intercepts:

  • Directional keys: ArrowUp, ArrowDown, ArrowLeft, ArrowRight
  • Boundary keys: Home, End, PageUp, PageDown

Under the hood, the component implements the roving tabindex pattern.

Code tip

  • For details about the keyboard patterns implemented here, see: W3C WAI-ARIA / Roving tabindex
  • For normative guidance on managing focus in composite widgets, see: WAI-ARIA 1.3 / Managing Focus and Supporting Keyboard Navigation (Information for Authors)
  • In practice for Composite, ensure your items are keyboard-focusable, keep focus movement predictable when content changes dynamically, and return to a logical active item when users re-enter the widget.
  • Composite uses a roving tabindex pattern (moving DOM focus between items), not aria-activedescendant.
  • To understand how the internal DOM sorting resolves dynamic rendering orders, look at the sortByDOMPosition utility inside the component's source code.

Basic invocation

The basic invocation of this primitive yields a hash of three different modifiers (composite, item, and group). Applying these modifiers registers the elements with the composite manager:

The primitive itself doesn't provide any visual styling or ARIA roles to the container or items, and doesn't generate any extra HTML beyond what is yielded. It solely provides the focus orchestration and keyboard routing functionalities to the elements the modifiers are applied to.

Setting an initial active item

Use @defaultCurrentId to set which registered item starts as active (tabindex="0"). The value should match the id of one of the elements using the item modifier.

Choosing a stable @defaultCurrentId can help preserve a consistent re-entry focus target, per WAI-ARIA focus management guidance.

<Hds::Composite @defaultCurrentId="second-item" as |C|>
  <div role="menu" {{C.composite}}>
    <button id="first-item" type="button" role="menuitem" {{C.item}}>
      First Item
    </button>
    <button id="second-item" type="button" role="menuitem" {{C.item}}>
      Second Item (Initially Active)
    </button>
    <button id="third-item" type="button" role="menuitem" {{C.item}}>
      Third Item
    </button>
  </div>
</Hds::Composite>

Notice: Set @defaultCurrentId equal to null if you want the composite container to receive initial focus instead of any item.

Constraining orientation

If your composite is strictly a horizontal toolbar or a vertical list, you can restrict the navigation axis using the @orientation argument.

  • Vertical Option A
  • Vertical Option B
  • Vertical Option C
<Hds::Composite @orientation="vertical" as |C|>
  <ul role="listbox" {{C.composite}}>
    <li role="option" tabindex="-1" {{C.item}}>
      Vertical Option A
    </li>
    <li role="option" tabindex="-1" {{C.item}}>
      Vertical Option B
    </li>
    <li role="option" tabindex="-1" {{C.item}}>
      Vertical Option C
    </li>
  </ul>
</Hds::Composite>

If set to vertical, left/right arrow keys will be ignored. If set to horizontal, up/down keys will be ignored.

Grouping and 2D navigation

Consumer responsibility

Because this component is completely headless, you are strictly responsible for providing the correct semantic WAI-ARIA roles (e.g., role="menu", role="menuitem", role="group", role="grid") to your HTML elements.

If you register groups using the group modifier, the component acts as a smart router, changing from 1D linear navigation to 2D grid navigation.

<Hds::Composite as |C|>
  <div role="grid" {{C.composite}}>
    <div role="row" {{C.group}}>
      <button type="button" role="gridcell" {{C.item}}>Row 1, Col 1</button>
      <button type="button" role="gridcell" {{C.item}}>Row 1, Col 2</button>
    </div>
    <div role="row" {{C.group}}>
      <button type="button" role="gridcell" {{C.item}}>Row 2, Col 1</button>
      <button type="button" role="gridcell" {{C.item}}>Row 2, Col 2</button>
    </div>
  </div>
</Hds::Composite>

When groups are present, ArrowRight/ArrowLeft navigate within the row (group), and ArrowDown/ArrowUp navigate the column index across groups. If an item does not exist at a specific column index in an adjacent group (an uneven grid), the routing logic will intelligently resolve to the closest enabled item.

Looping and wrapping

You can control how the focus behaves when a user reaches the start or end of the composite using the @loop and @wrap arguments.

  • Looping (@loop): If true, reaching the end of a row/column will cycle focus back to the beginning of that same row/column.
  • Wrapping (@wrap): If true (and in a 2D grouped context), reaching the end of a row will move focus to the beginning of the next row.

Use @loop when you want to cycle inside the same row/column:

<Hds::Composite @loop={{true}} as |C|>
  <div role="grid" {{C.composite}}>
    <div role="row" {{C.group}}>
      <button type="button" role="gridcell" {{C.item}}>Start</button>
      <button type="button" role="gridcell" {{C.item}}>Middle</button>
      <button type="button" role="gridcell" {{C.item}}>End (Loops to Start)</button>
    </div>
    <div role="row" {{C.group}}>
      <button type="button" role="gridcell" {{C.item}}>Next Row Start</button>
      <button type="button" role="gridcell" {{C.item}}>Next Row End</button>
    </div>
  </div>
</Hds::Composite>

Use @wrap when you want to move into the next/previous row or column in grouped layouts:

<Hds::Composite @wrap={{true}} as |C|>
  <div role="grid" {{C.composite}}>
    <div role="row" {{C.group}}>
      <button type="button" role="gridcell" {{C.item}}>Start</button>
      <button type="button" role="gridcell" {{C.item}}>Middle</button>
      <button type="button" role="gridcell" {{C.item}}>End (Wraps to Next Row)</button>
    </div>
    <div role="row" {{C.group}}>
      <button type="button" role="gridcell" {{C.item}}>Next Row Start</button>
      <button type="button" role="gridcell" {{C.item}}>Next Row End</button>
    </div>
  </div>
</Hds::Composite>

Both arguments accept either a boolean (true/false) or a specific axis string ('horizontal' or 'vertical') if you only want to loop/wrap in one direction.

If both are enabled for the same axis, looping takes precedence and wrapping is not applied for that key press.

Disabled items

You can pass a disabled named argument directly to the item modifier. The component will automatically apply the correct disabled and aria-disabled attributes to the DOM node, and the internal navigation logic will intelligently skip over it when routing keyboard events.

<Hds::Composite as |C|>
  <div role="menu" {{C.composite}}>
    <button type="button" role="menuitem" {{C.item}}>
      Active Item
    </button>

    {{! The disabled argument prevents focus routing and applies disabled attributes }}
    <button type="button" role="menuitem" {{C.item disabled=true}}>
      Disabled Item (Skipped)
    </button>

    <button type="button" role="menuitem" {{C.item}}>
      Another Active Item
    </button>
  </div>
</Hds::Composite>

Component API

Composite

[C].composite yielded modifier
The composite modifier yielded as a contextual modifier (see below).
[C].group yielded modifier
The group modifier yielded as a contextual modifier (see below).
[C].item yielded modifier
The item modifier yielded as a contextual modifier (see below).
defaultCurrentId string | null
The ID of the item that should hold the initial active state (and tabindex="0"). If explicitly set to null, no item is initially active and the composite container itself receives the focus. If left unset, the component automatically selects the first enabled item.
loop boolean | string
  • true
  • false (default)
  • horizontal
  • vertical
Controls whether navigation loops back to the beginning (or end) of the current row/column when a boundary is reached. Accepts a boolean to apply to all axes, or a specific axis string ("horizontal" or "vertical").
orientation string
  • horizontal
  • vertical
Restricts keyboard navigation to a single axis. If set to "vertical", Left/Right arrow keys are ignored. If set to "horizontal", Up/Down arrow keys are ignored. If left undefined, the component enables 2D navigation when groups are present.
wrap boolean | string
  • true
  • false (default)
  • horizontal
  • vertical
Controls whether navigation moves to the beginning of the next row/column when reaching the end of the current one in a 2D grouped context. Accepts a boolean to apply to all axes, or a specific axis string ("horizontal" or "vertical").

Contextual modifiers

Because Composite is a headless component, it yields element modifiers rather than UI components.

[C].composite

The container modifier, yielded contextually.

element HTMLElement
The element this modifier is applied to becomes the root controller. It attaches the core keyboard event listeners (keydown) and manages the roving tabindex across its registered descendants.

[C].group

The grouping modifier, yielded contextually.

element HTMLElement
Registers the DOM element as a structural group. Used internally by the composite manager to orchestrate 2D grid navigation, calculate adjacent wrapping bounds, and determine column/row indexes.

[C].item

The interactive element modifier, yielded contextually.

element HTMLElement
Registers the element as a navigable item within the composite. It attaches a focus event listener to ensure mouse interactions keep the internal state synced with keyboard navigation.
disabled boolean
  • false (default)
Passed as a named argument to the modifier (e.g., <button >). When true, the modifier automatically applies the disabled and aria-disabled="true" attributes to the element, and ensures the item is safely skipped during keyboard routing.

Related