Skip to content

Toggle Group

Low-level toggle group primitive that groups buttons with single or multiple selection, roving tabindex keyboard navigation, and ARIA toolbar semantics.

Updated View as Markdown

Toggle Group

A set of toggleable buttons where one or more can be pressed at a time. ToggleGroup supports single and multiple selection modes, roving tabindex keyboard navigation, and full ARIA attributes for assistive technology.

import { ToggleGroup } from "@areia/slots";

document.querySelector("#root")!.innerHTML = `
  <div data-slot="toggle-group" data-orientation="horizontal" class="win95-inset inline-flex">
    <button data-slot="toggle-group-item" data-value="left" class="win95-button border-0 border-r border-black last:border-r-0 data-[state=on]:bg-white">
      Left
    </button>
    <button data-slot="toggle-group-item" data-value="center" class="win95-button border-0 border-r border-black last:border-r-0 data-[state=on]:bg-white">
      Center
    </button>
    <button data-slot="toggle-group-item" data-value="right" class="win95-button border-0 border-r border-black last:border-r-0 data-[state=on]:bg-white">
      Right
    </button>
  </div>
`;

const root = document.querySelector(
  '[data-slot="toggle-group"]',
)!;
const controller = ToggleGroup.createToggleGroup(root, {
  defaultValue: "center",
  orientation: "horizontal",
});

Import

import { ToggleGroup } from "@areia/slots";

Usage

Add data-slot="toggle-group" to a container element and place button elements inside with data-slot="toggle-group-item" and unique data-value attributes. Call ToggleGroup.createToggleGroup(root) to initialize the controller.

import { ToggleGroup } from "@areia/slots";

// Auto-bind a single root
const root = document.querySelector(
  '[data-slot="toggle-group"]',
)!;
const controller = ToggleGroup.createToggleGroup(root, {
  defaultValue: "bold",
  onValueChange: (value) => {
    console.log("Selection changed:", value);
  },
});

// Auto-bind all unbound roots in scope
const controllers = ToggleGroup.create();

Single vs Multiple Mode

By default, only one item can be selected at a time — pressing another item deselects the previous one. Toggling the selected item deselects it.

Set the multiple option or add data-multiple on the root to allow multiple selections. In multiple mode, each press toggles the item independently.

<!-- Single mode (default) -->
<div data-slot="toggle-group" data-orientation="horizontal">
  <button data-slot="toggle-group-item" data-value="left">
    Left
  </button>
  <button data-slot="toggle-group-item" data-value="center">
    Center
  </button>
  <button data-slot="toggle-group-item" data-value="right">
    Right
  </button>
</div>

<!-- Multiple mode -->
<div
  data-slot="toggle-group"
  data-multiple
  data-orientation="horizontal"
>
  <button data-slot="toggle-group-item" data-value="bold">
    B
  </button>
  <button data-slot="toggle-group-item" data-value="italic">
    I
  </button>
  <button data-slot="toggle-group-item" data-value="underline">
    U
  </button>
</div>

Expected Markup

The controller expects a root element with data-slot="toggle-group" containing items with data-slot="toggle-group-item".

<div data-slot="toggle-group" data-orientation="horizontal">
  <button data-slot="toggle-group-item" data-value="left">
    Left
  </button>
  <button data-slot="toggle-group-item" data-value="center">
    Center
  </button>
  <button
    data-slot="toggle-group-item"
    data-value="right"
    disabled
  >
    Right
  </button>
</div>

Data Slots

Slot Required Description
toggle-group Yes Root container. Receives role="group".
toggle-group-item Yes A toggleable button within the group. Each must have a data-value.

Generated Attributes

The controller writes the following attributes on initialization and updates them as state changes:

Attribute Element(s) Description
data-state toggle-group-item "on" when pressed, "off" when not.
data-multiple Root Present when multiple is enabled.
data-value Root Space-separated list of currently selected values.
data-disabled Root Present when the entire group is disabled.
data-orientation Root, items "horizontal" or "vertical".
role Root Set to "group".
aria-orientation Root "vertical" when vertical, omitted when horizontal.
aria-pressed toggle-group-item "true" when pressed, "false" when not.
aria-disabled Root, items "true" when disabled, removed otherwise.
tabindex toggle-group-item 0 on the roving focus target, -1 on all others.
type toggle-group-item (if <button>) Set to "button" to prevent form submission.

Items without a data-value attribute become non-interactive: tabindex="-1", aria-disabled="true".

Keyboard Navigation

Arrow keys move focus between items using a roving tabindex. The first selected item receives initial focus; if none are selected, the first enabled item gets focus.

Key Behavior
ArrowRight / ArrowDown Move focus to next item (next column in vertical).
ArrowLeft / ArrowUp Move focus to previous item (previous column in vertical).
Home Move focus to first item.
End Move focus to last item.
Enter / Space Toggle the focused item.

When loop is true (default), arrow navigation wraps from the last item to the first and vice versa.

Events

Outbound

Event Detail Description
toggle-group:change { value: string[] } Fired on the root when the selection changes via user interaction or the controller.

Inbound

Event Detail Description
toggle-group:set { value: string | string[] } | string | string[] Dispatch on the root to set the selection programmatically. The preferred shape is { value: ... }. String and array shorthand are also accepted.

API Reference

Options

Option Type Default Description
defaultValue string | string[] Initial selected value(s). Use a string for single mode, an array for multiple mode.
multiple boolean false Allow more than one item to be selected at the same time.
orientation "horizontal" | "vertical" "horizontal" Layout direction. Controls roving-focus axis and ARIA attributes.
loop boolean true When true, arrow-key navigation wraps from the last item to the first (and vice versa).
disabled boolean false Disable the entire toggle group. Blocks user clicks, keyboard interaction, and inbound events.
onValueChange (value: string[]) => void Called when the selected values change.

Controller

Member Type Description
value string[] (getter) Currently selected values.
setValue(value) (value: string | string[]) => void Set the selection to the given value(s).
toggle(value) (value: string) => void Toggle the item identified by value.
destroy() () => void Remove all event listeners and clean up.

Controller

The controller is returned by ToggleGroup.createToggleGroup() and provides imperative control over the toggle group’s state.

import { ToggleGroup } from "@areia/slots";

const root = document.querySelector(
  '[data-slot="toggle-group"]',
)!;
const controller = ToggleGroup.createToggleGroup(root, {
  defaultValue: "center",
  onValueChange: (value) => {
    console.log("Selection:", value);
  },
});

// Set selection programmatically
controller.setValue("right");

// Set multiple values (requires multiple mode)
controller.setValue(["bold", "italic"]);

// Toggle a specific item
controller.toggle("center");

// Read current state
console.log(controller.value); // ["center"]

// Tear down
controller.destroy();

Listening to Events

const root = document.querySelector(
  '[data-slot="toggle-group"]',
)!;

root.addEventListener("toggle-group:change", (event) => {
  const { value } = event.detail;
  console.log("Selection changed:", value);
});

Inbound Events

Dispatch toggle-group:set on the root to set the selection from outside the controller. The event is ignored when the group is disabled.

const root = document.querySelector(
  '[data-slot="toggle-group"]',
)!;

// Preferred shape
root.dispatchEvent(
  new CustomEvent("toggle-group:set", {
    detail: { value: "bold" },
  }),
);

// Multiple values (requires multiple mode)
root.dispatchEvent(
  new CustomEvent("toggle-group:set", {
    detail: { value: ["bold", "italic"] },
  }),
);

// String shorthand
root.dispatchEvent(
  new CustomEvent("toggle-group:set", {
    detail: "left",
  }),
);

Accessibility

  • The root element receives role="group".
  • aria-orientation is set on the root when orientation is "vertical".
  • Each item receives aria-pressed="true" or "false".
  • Disabled items receive aria-disabled="true".
  • Items without a data-value attribute are made non-interactive with tabindex="-1" and aria-disabled="true".
  • Roving tabindex ensures only one item in the group is focusable at a time.
  • Enter and Space toggle the focused item via native button behavior.
  • Items that are <button> elements have type="button" set automatically to prevent unintended form submission.
Navigation

Type to search…

↑↓ navigate↵ selectEsc close