Toggle Group
stimeo--toggle-group
A row of buttons that hold a pressed state, for picking one or picking several.
A row of buttons that hold a pressed state. Whether one is pressed is announced while its name stays the same, because a name that swaps underneath confuses anyone relying on it. The whole row is a single Tab stop and the arrow keys move within it. It can take one choice or several. A button that cannot be used is skipped by the arrow keys, while one marked unavailable stays reachable but does nothing. Buttons added later work without setup of their own. You are told only when someone actually pressed one.
Multiple selection
Single selection
Unavailable items
Keyboard
| Key | Action |
|---|---|
| Space / Enter | Toggle the focused button. |
| → / ↓ | Move focus to the next button (wraps). Under RTL only the horizontal pair swaps. |
| ← / ↑ | Move focus to the previous button (wraps). Under RTL only the horizontal pair swaps. |
| Home / End | Move focus to the first / last button. |
<%# Markup for the toggle-group demo: multiple selection, single selection, and
unavailable items. The group delegates click and keydown, so the items carry
no data-action of their own. %>
<div class="toggle-group-demo-suite">
<section class="toggle-group-demo__example">
<h3 class="toggle-group-demo__heading" id="toggle-group-multiple-heading">
<%= t("components.toggle_group.demo.multiple_heading") %>
</h3>
<div class="toggle-group-demo" role="group"
aria-labelledby="toggle-group-multiple-heading"
data-controller="stimeo--toggle-group">
<button type="button" class="toggle-group-demo__item" aria-pressed="false" tabindex="0"
aria-label="<%= t('components.toggle_group.demo.bold') %>" data-value="bold"
data-stimeo--toggle-group-target="item">
<svg class="demo-icon" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M6 4h8a4 4 0 0 1 0 8H6z"></path>
<path d="M6 12h9a4 4 0 0 1 0 8H6z"></path>
</svg>
</button>
<button type="button" class="toggle-group-demo__item" aria-pressed="false" tabindex="-1"
aria-label="<%= t('components.toggle_group.demo.italic') %>" data-value="italic"
data-stimeo--toggle-group-target="item">
<svg class="demo-icon" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<line x1="19" y1="4" x2="10" y2="4"></line>
<line x1="14" y1="20" x2="5" y2="20"></line>
<line x1="15" y1="4" x2="9" y2="20"></line>
</svg>
</button>
<button type="button" class="toggle-group-demo__item" aria-pressed="false" tabindex="-1"
aria-label="<%= t('components.toggle_group.demo.underline') %>" data-value="underline"
data-stimeo--toggle-group-target="item">
<svg class="demo-icon" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M6 3v7a6 6 0 0 0 12 0V3"></path>
<line x1="4" y1="21" x2="20" y2="21"></line>
</svg>
</button>
</div>
</section>
<section class="toggle-group-demo__example">
<h3 class="toggle-group-demo__heading" id="toggle-group-single-heading">
<%= t("components.toggle_group.demo.single_heading") %>
</h3>
<div class="toggle-group-demo" role="group"
aria-labelledby="toggle-group-single-heading"
data-controller="stimeo--toggle-group"
data-stimeo--toggle-group-mode-value="single">
<button type="button" class="toggle-group-demo__item" aria-pressed="true" tabindex="0"
aria-label="<%= t('components.toggle_group.demo.align_left') %>" data-value="left"
data-stimeo--toggle-group-target="item">
<svg class="demo-icon" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2"
stroke-linecap="round" aria-hidden="true">
<path d="M4 6h16M4 10h11M4 14h16M4 18h9"></path>
</svg>
</button>
<button type="button" class="toggle-group-demo__item" aria-pressed="false" tabindex="-1"
aria-label="<%= t('components.toggle_group.demo.align_center') %>" data-value="center"
data-stimeo--toggle-group-target="item">
<svg class="demo-icon" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2"
stroke-linecap="round" aria-hidden="true">
<path d="M4 6h16M6 10h12M4 14h16M7 18h10"></path>
</svg>
</button>
<button type="button" class="toggle-group-demo__item" aria-pressed="false" tabindex="-1"
aria-label="<%= t('components.toggle_group.demo.align_right') %>" data-value="right"
data-stimeo--toggle-group-target="item">
<svg class="demo-icon" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2"
stroke-linecap="round" aria-hidden="true">
<path d="M4 6h16M9 10h11M4 14h16M11 18h9"></path>
</svg>
</button>
</div>
</section>
<section class="toggle-group-demo__example">
<h3 class="toggle-group-demo__heading" id="toggle-group-disabled-heading">
<%= t("components.toggle_group.demo.disabled_heading") %>
</h3>
<div class="toggle-group-demo" role="group"
aria-labelledby="toggle-group-disabled-heading"
data-controller="stimeo--toggle-group">
<button type="button" class="toggle-group-demo__item toggle-group-demo__item--wide"
aria-pressed="false" tabindex="0" data-value="available"
data-stimeo--toggle-group-target="item">
<%= t("components.toggle_group.demo.available") %>
</button>
<button type="button" class="toggle-group-demo__item toggle-group-demo__item--wide"
aria-pressed="false" tabindex="-1" data-value="native-disabled" disabled
data-stimeo--toggle-group-target="item">
<%= t("components.toggle_group.demo.native_disabled") %>
</button>
<button type="button" class="toggle-group-demo__item toggle-group-demo__item--wide"
aria-pressed="false" tabindex="-1" data-value="aria-disabled" aria-disabled="true"
data-stimeo--toggle-group-target="item">
<%= t("components.toggle_group.demo.aria_disabled") %>
</button>
</div>
</section>
</div>
/*
* Presentation-only styles for the toggle-group demo.
* The pressed state is expressed via [aria-pressed="true"] (set by the library).
*/
.toggle-group-demo-suite {
display: grid;
gap: 1.5rem;
}
.toggle-group-demo__example {
display: grid;
justify-items: start;
gap: 0.5rem;
}
.toggle-group-demo__heading {
margin: 0;
color: var(--fg);
font-size: 0.9375rem;
font-weight: 700;
}
.toggle-group-demo {
display: inline-flex;
gap: 0.25rem;
padding: 0.25rem;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 0.5rem;
}
.toggle-group-demo__item {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.25rem;
height: 2.25rem;
color: var(--fg);
background: transparent;
border: 1px solid transparent;
border-radius: 0.375rem;
cursor: pointer;
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.toggle-group-demo__item--wide {
width: auto;
min-width: 2.25rem;
padding-inline: 0.75rem;
font: inherit;
font-size: 0.875rem;
}
.toggle-group-demo__item:hover {
background: var(--surface-subtle);
}
.toggle-group-demo__item:disabled,
.toggle-group-demo__item[aria-disabled="true"] {
opacity: 0.5;
cursor: not-allowed;
}
.toggle-group-demo__item:disabled:hover,
.toggle-group-demo__item[aria-disabled="true"]:hover {
background: transparent;
}
.toggle-group-demo__item:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
/* Fill with the accent color while pressed. */
.toggle-group-demo__item[aria-pressed="true"] {
color: var(--white);
background: var(--accent);
border-color: var(--accent);
}
.toggle-group-demo__item .demo-icon {
width: 1.1rem;
height: 1.1rem;
}
This demo needs no consumer-side JS (the controller handles the behavior).
These demo styles use shared design tokens (light + dark). Copy the shared styles too, then toggle data-theme on your root element for dark mode.
The data-* attributes you add to your own HTML to wire this component. Put the data-controller below on a root element, then place its targets / values / actions inside that element.
On the root element
data-controller="stimeo--toggle-group"
Targets
| Name | Description | Attribute |
|---|---|---|
item
required
|
A <button type="button"> or non-interactive role="button" toggle; pressed state is aria-pressed. |
data-stimeo--toggle-group-target="item" |
Values
| Name | Description | Attribute |
|---|---|---|
mode
|
single (0–1 pressed, others released) or multiple (any number; default). |
data-stimeo--toggle-group-mode-value |
Actions
| Name | Description | Action |
|---|---|---|
onKeydown
|
Optional per-item action; Arrow/Home/End rove and Space/Enter activate generic hosts. | stimeo--toggle-group#onKeydown |
toggle
|
Optional per-item action that toggles the item. Delegated click/keydown work without either action. | stimeo--toggle-group#toggle |
Events
| Name | Description | Event |
|---|---|---|
change
|
User activation only; detail is { value, pressed, values } in DOM order. |
stimeo--toggle-group:change |
State hooks
The library only manages these ARIA/data attributes and custom properties. Your CSS reads them to render the look — selectors like [aria-selected], [aria-expanded], or var(--stimeo--…) hook into this state.
| Hook | Target | Meaning |
|---|---|---|
aria-pressed |
Item | "true" / "false" — the button's pressed state. |
tabindex |
Item | 0 on the active item, -1 on the rest (roving). |