Toggle Group
stimeo--toggle-group
Toggle buttons with aria-pressed, single/multiple selection, and Toolbar-style roving.
The stimeo--toggle-group controller implements the APG Button (toggle) pattern with Toolbar-style roving navigation. Each item's pressed state is aria-pressed (the accessible name never changes) and the group is a single Tab stop. In single mode pressing one item releases the others; multiple allows any number. Per the Toolbar model the arrow keys move focus only — activation is Space/Enter (or click). stimeo--toggle-group:change is dispatched on every toggle.
Keyboard
| Key | Action |
|---|---|
| Space / Enter | Toggle the focused button. |
| → / ↓ | Move focus to the next button (wraps). |
| ← / ↑ | Move focus to the previous button (wraps). |
| Home / End | Move focus to the first / last button. |
<%# Markup for the toggle-group (APG Button toggle + Toolbar roving) demo.
Pressed state is aria-pressed; the single tab stop is roving tabindex. Arrows only
move focus; toggling is Space/Enter and click. Multiple selection (multiple). The look
switches on [aria-pressed="true"]. %>
<div class="toggle-group-demo" role="group"
aria-label="<%= t('components.toggle_group.demo.label') %>"
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"
data-action="click->stimeo--toggle-group#toggle
keydown->stimeo--toggle-group#onKeydown">
<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"
data-action="click->stimeo--toggle-group#toggle
keydown->stimeo--toggle-group#onKeydown">
<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"
data-action="click->stimeo--toggle-group#toggle
keydown->stimeo--toggle-group#onKeydown">
<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>
/*
* Presentation-only styles for the toggle-group demo.
* The pressed state is expressed via [aria-pressed="true"] (set by the library).
*/
.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:hover {
border-color: var(--accent);
}
.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 toggle button in the group (role/button); pressed state via 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
|
Arrow/Home/End move focus only (wrapping); Space/Enter toggle non-button hosts. | stimeo--toggle-group#onKeydown |
toggle
|
Toggles the activated item per the current mode. | stimeo--toggle-group#toggle |
Events
| Name | Description | Event |
|---|---|---|
change
|
Dispatched on every toggle, with the value, its pressed state, and all pressed values in detail. | 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). |