Skip to content

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

SlotRequiredDescription
toggle-groupYesRoot container. Receives role="group".
toggle-group-itemYesA 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:

AttributeElement(s)Description
data-statetoggle-group-item"on" when pressed, "off" when not.
data-multipleRootPresent when multiple is enabled.
data-valueRootSpace-separated list of currently selected values.
data-disabledRootPresent when the entire group is disabled.
data-orientationRoot, items"horizontal" or "vertical".
roleRootSet to "group".
aria-orientationRoot"vertical" when vertical, omitted when horizontal.
aria-pressedtoggle-group-item"true" when pressed, "false" when not.
aria-disabledRoot, items"true" when disabled, removed otherwise.
tabindextoggle-group-item0 on the roving focus target, -1 on all others.
typetoggle-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.

KeyBehavior
ArrowRight / ArrowDownMove focus to next item (next column in vertical).
ArrowLeft / ArrowUpMove focus to previous item (previous column in vertical).
HomeMove focus to first item.
EndMove focus to last item.
Enter / SpaceToggle the focused item.

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

Events

Outbound

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

Inbound

EventDetailDescription
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

OptionTypeDefaultDescription
defaultValuestring | string[]Initial selected value(s). Use a string for single mode, an array for multiple mode.
multiplebooleanfalseAllow more than one item to be selected at the same time.
orientation"horizontal" | "vertical""horizontal"Layout direction. Controls roving-focus axis and ARIA attributes.
loopbooleantrueWhen true, arrow-key navigation wraps from the last item to the first (and vice versa).
disabledbooleanfalseDisable the entire toggle group. Blocks user clicks, keyboard interaction, and inbound events.
onValueChange(value: string[]) => voidCalled when the selected values change.

Controller

MemberTypeDescription
valuestring[] (getter)Currently selected values.
setValue(value)(value: string | string[]) => voidSet the selection to the given value(s).
toggle(value)(value: string) => voidToggle the item identified by value.
destroy()() => voidRemove 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.