Toggle group
A pill container that hosts a row of toggles pressed inside its padded interior.
For a single press button, see Toggle. For purely visual button grouping with no state, see Button group.
Single-select (segmented control)
Use role="radiogroup" on the wrapper and role="radio" with aria-checked on each member. The CSS hook is data-state="active" on the current member. Add data-stisla-toggle-group to wire the click and arrow-key contract. The type autodetects from role="radiogroup".
<div class="toggle-group" data-stisla-toggle-group role="radiogroup" aria-label="Text alignment">
<button type="button" class="toggle toggle--icon-only" role="radio" aria-checked="false" aria-label="Align left" data-value="left">
<i data-lucide="align-left"></i>
</button>
<button type="button" class="toggle toggle--icon-only" role="radio" data-state="active" aria-checked="true" aria-label="Align center" data-value="center">
<i data-lucide="align-center"></i>
</button>
<button type="button" class="toggle toggle--icon-only" role="radio" aria-checked="false" aria-label="Align right" data-value="right">
<i data-lucide="align-right"></i>
</button>
</div>Multi-select
Each member is an independent press toggle. Use role="group" on the wrapper and aria-pressed on each member. The type autodetects to multiple when no radio role is present; arrows move focus, Space or Enter commits the press.
<div class="toggle-group" data-stisla-toggle-group role="group" aria-label="Text style">
<button type="button" class="toggle toggle--icon-only" aria-pressed="true" aria-label="Bold" data-value="bold">
<i data-lucide="bold"></i>
</button>
<button type="button" class="toggle toggle--icon-only" aria-pressed="false" aria-label="Italic" data-value="italic">
<i data-lucide="italic"></i>
</button>
<button type="button" class="toggle toggle--icon-only" aria-pressed="true" aria-label="Underline" data-value="underline">
<i data-lucide="underline"></i>
</button>
</div>With text labels
Members can carry text instead of (or alongside) icons. The group's intrinsic width grows to fit.
<div class="toggle-group" data-stisla-toggle-group role="radiogroup" aria-label="Calendar view">
<button type="button" class="toggle" role="radio" data-state="active" aria-checked="true" data-value="day">Day</button>
<button type="button" class="toggle" role="radio" aria-checked="false" data-value="week">Week</button>
<button type="button" class="toggle" role="radio" aria-checked="false" data-value="month">Month</button>
</div>Icon + label
Mix icons with text by placing an <i> inside the .toggle button next to the label.
<div class="toggle-group" data-stisla-toggle-group role="radiogroup" aria-label="View mode">
<button type="button" class="toggle" role="radio" data-state="active" aria-checked="true" data-value="list">
<i data-lucide="list"></i>
List
</button>
<button type="button" class="toggle" role="radio" aria-checked="false" data-value="grid">
<i data-lucide="layout-grid"></i>
Grid
</button>
<button type="button" class="toggle" role="radio" aria-checked="false" data-value="kanban">
<i data-lucide="columns-3"></i>
Kanban
</button>
</div>Form data, radio set
For single-select that needs to submit with a form, use native radios. The hidden <input class="toggle-input" type="radio"> drives the paired <label class="toggle">; the browser handles single-select natively. No JS required.
<div class="toggle-group">
<input type="radio" class="toggle-input" name="planRange" id="planDay" value="day" checked>
<label class="toggle" for="planDay">Day</label>
<input type="radio" class="toggle-input" name="planRange" id="planWeek" value="week">
<label class="toggle" for="planWeek">Week</label>
<input type="radio" class="toggle-input" name="planRange" id="planMonth" value="month">
<label class="toggle" for="planMonth">Month</label>
</div>Form data, checkbox set
For multi-select form data, use native checkboxes the same way. Multiple labels can be active at once.
<div class="toggle-group">
<input type="checkbox" class="toggle-input" name="tools" id="toolBold" value="bold" checked>
<label class="toggle toggle--icon-only" for="toolBold" aria-label="Bold">
<i data-lucide="bold"></i>
</label>
<input type="checkbox" class="toggle-input" name="tools" id="toolItalic" value="italic">
<label class="toggle toggle--icon-only" for="toolItalic" aria-label="Italic">
<i data-lucide="italic"></i>
</label>
<input type="checkbox" class="toggle-input" name="tools" id="toolUnderline" value="underline" checked>
<label class="toggle toggle--icon-only" for="toolUnderline" aria-label="Underline">
<i data-lucide="underline"></i>
</label>
</div>Sizes
Add .toggle-group--sm or .toggle-group--lg. Outer height shrinks to 28 / grows to 44 px under default density; the inner toggle height re-derives from the new container values automatically (concentric corners per V3.md §3.4).
<div class="d-flex flex-column gap-2 align-items-start">
<div class="toggle-group toggle-group--sm" data-stisla-toggle-group role="radiogroup" aria-label="Small">
<button type="button" class="toggle" role="radio" data-state="active" aria-checked="true">Day</button>
<button type="button" class="toggle" role="radio" aria-checked="false">Week</button>
<button type="button" class="toggle" role="radio" aria-checked="false">Month</button>
</div>
<div class="toggle-group" data-stisla-toggle-group role="radiogroup" aria-label="Default">
<button type="button" class="toggle" role="radio" data-state="active" aria-checked="true">Day</button>
<button type="button" class="toggle" role="radio" aria-checked="false">Week</button>
<button type="button" class="toggle" role="radio" aria-checked="false">Month</button>
</div>
<div class="toggle-group toggle-group--lg" data-stisla-toggle-group role="radiogroup" aria-label="Large">
<button type="button" class="toggle" role="radio" data-state="active" aria-checked="true">Day</button>
<button type="button" class="toggle" role="radio" aria-checked="false">Week</button>
<button type="button" class="toggle" role="radio" aria-checked="false">Month</button>
</div>
</div>Vertical
Add .toggle-group--vertical to stack members. The container is block-level so it fills its parent's width. Wrap in a sized container (sidebar shell, settings panel) to constrain. Inner toggles keep their standalone default height; content left-aligns as a menu list.
<div style="max-width: 240px;">
<div class="toggle-group toggle-group--vertical" data-stisla-toggle-group role="radiogroup" aria-label="Sidebar view">
<button type="button" class="toggle" role="radio" data-state="active" aria-checked="true" data-value="inbox">
<i data-lucide="inbox"></i>
Inbox
</button>
<button type="button" class="toggle" role="radio" aria-checked="false" data-value="archive">
<i data-lucide="archive"></i>
Archive
</button>
<button type="button" class="toggle" role="radio" aria-checked="false" data-value="trash">
<i data-lucide="trash-2"></i>
Trash
</button>
</div>
</div>Keyboard
Roving tabindex keeps one member in the tab order at a time, so Tab leaves the group naturally. Arrow keys move focus along the group's orientation. In single-select mode focus is selection (WAI-ARIA radio-group); multi-select decouples them.
| Key | Single-select | Multi-select |
|---|---|---|
| → / ↓ | Focus next enabled member, auto-select. | Focus next enabled member. |
| ← / ↑ | Focus previous enabled member, auto-select. | Focus previous enabled member. |
| Home | Focus first enabled, auto-select. | Focus first enabled member. |
| End | Focus last enabled, auto-select. | Focus last enabled member. |
| Space / Enter | Select focused (no-op if already selected). | Flip aria-pressed on focused member. |
| Tab | Leaves the group. Only the tabbable member is in the tab order. | |
Customization
Six variables retune .toggle-group. Child .toggle vars (height, radius, bg, border) re-derive from the container values inside member scope. See Toggle for the child surface.
| Variable | Default | Use |
|---|---|---|
--toggle-group-radius |
var(--st-toggle-group-radius, var(--st-radius)) | Outer corner radius. Inner toggle radius computes as outer - padding (concentric). |
--toggle-group-height |
2.25rem | Outer raw height (density multiplies once). Inner toggle fills via flex stretch, so height resolves to outer − 2 × border − 2 × padding via the box model. |
--toggle-group-padding |
0.125rem | Padding around the children. Subtracts from the inner toggle's effective height and from the concentric inner radius. |
--toggle-group-gap |
0.125rem | Space between adjacent toggles. Lets multi-active members breathe without border collisions. |
--toggle-group-bg |
transparent | Container fill. Set to var(--st-surface-2) for a tinted pill. |
--toggle-group-border-color |
var(--st-border) | Outer rim color. |