API
Toggle group
Toggles joined into one control, single or multiple selection.
Installation
pnpm dlx zazz-ui add toggle-groupnpx zazz-ui add toggle-groupyarn dlx zazz-ui add toggle-groupbunx zazz-ui add toggle-groupAlso adds fields, kbd, button, toggle, which it depends on. Run zazz-ui init first if the project has no zazz/ folder yet.
Examples
Default
<ui-toggle-group role="group" aria-label="Text alignment">
<label data-ui="toggle" data-toggle-size="icon">
<input type="radio" name="align" aria-label="Align left" checked />
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<line x1="4" y1="6" x2="20" y2="6" />
<line x1="4" y1="12" x2="14" y2="12" />
<line x1="4" y1="18" x2="18" y2="18" />
</svg>
</label>
<label data-ui="toggle" data-toggle-size="icon">
<input type="radio" name="align" aria-label="Align center" />
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<line x1="4" y1="6" x2="20" y2="6" />
<line x1="7" y1="12" x2="17" y2="12" />
<line x1="5" y1="18" x2="19" y2="18" />
</svg>
</label>
<label data-ui="toggle" data-toggle-size="icon">
<input type="radio" name="align" aria-label="Align right" />
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<line x1="4" y1="6" x2="20" y2="6" />
<line x1="10" y1="12" x2="20" y2="12" />
<line x1="6" y1="18" x2="20" y2="18" />
</svg>
</label>
</ui-toggle-group>/**
* toggle-group.css: Toggle group (ui-toggle-group | [data-ui~="toggle-group"])
*
* @layer variables, components
* @requires _layers.css, _variables.css, _properties.css (--_*-resolved), toggle.css
* @uses :not(): first/last edge selection for shared-border collapse
* @uses logical border-radius longhands: RTL-safe corner rounding per orientation
* @uses flex-item z-index: raise the active control so its border/ring isn't clipped
* @tokens --ui-toggle-group-* (@layer variables; named after the utilities they back)
*/
@layer variables {
:root {
--ui-toggle-group-rounded: var(--ui-toggle-rounded);
--ui-toggle-group-bg: var(--color-card);
}
}
@property --_toggle-group-rounded {
syntax: "*";
inherits: false;
}
@property --_toggle-group-bg {
syntax: "*";
inherits: false;
}
@layer ui.components {
/* ===========================================================================
TOGGLE GROUP
Joins adjacent toggles into a single segmented unit: a single-select set
(radios sharing a `name`) or a multi-select set (checkboxes). Children keep
their own --ui-toggle-rounded, so the outer corners follow the toggles' size
preset automatically; only the *inner* corners are squared off and the
doubled border is collapsed by dropping
each later control's leading border (no overlap).
=========================================================================== */
:where(ui-toggle-group, [data-ui~="toggle-group"]) {
display: flex;
inline-size: fit-content;
/* stretch so mixed-height children share one edge */
align-items: stretch;
border-radius: var(
--_rounded-resolved,
var(--_toggle-group-rounded, var(--ui-toggle-group-rounded))
);
background-color: var(--_bg-resolved, var(--_toggle-group-bg, var(--ui-toggle-group-bg)));
position: relative;
isolation: isolate;
}
/* flex items honour z-index without positioning, but relative keeps the
stacking context robust across child types */
:where(ui-toggle-group, [data-ui~="toggle-group"]) > * {
position: relative;
}
/* the focused/hovered/checked toggle floats above its neighbors so its
focus ring paints over its neighbors */
:where(ui-toggle-group, [data-ui~="toggle-group"]) > *:hover:not(:has(> input:disabled)),
:where(ui-toggle-group, [data-ui~="toggle-group"]) > *:focus-within,
:where(ui-toggle-group, [data-ui~="toggle-group"]) > *:has(> input:checked) {
z-index: 1;
}
/* ===========================================================================
TOGGLE GROUP: HORIZONTAL (default)
=========================================================================== */
:where(ui-toggle-group, [data-ui~="toggle-group"]):not(
[data-toggle-group-orientation="vertical"]
) {
flex-direction: row;
}
:where(ui-toggle-group, [data-ui~="toggle-group"]):not([data-toggle-group-orientation="vertical"])
> :not(:last-child) {
border-start-end-radius: 0;
border-end-end-radius: 0;
}
:where(ui-toggle-group, [data-ui~="toggle-group"]):not([data-toggle-group-orientation="vertical"])
> :not(:first-child) {
/* drop the leading border so neighbors share one seam, not two */
border-inline-start-width: 0;
border-start-start-radius: 0;
border-end-start-radius: 0;
}
/* ===========================================================================
TOGGLE GROUP: VERTICAL — data-toggle-group-orientation="vertical"
=========================================================================== */
:where(
ui-toggle-group[data-toggle-group-orientation="vertical"],
[data-ui~="toggle-group"][data-toggle-group-orientation="vertical"]
) {
flex-direction: column;
}
:where(
ui-toggle-group[data-toggle-group-orientation="vertical"],
[data-ui~="toggle-group"][data-toggle-group-orientation="vertical"]
)
> :not(:last-child) {
border-end-start-radius: 0;
border-end-end-radius: 0;
}
:where(
ui-toggle-group[data-toggle-group-orientation="vertical"],
[data-ui~="toggle-group"][data-toggle-group-orientation="vertical"]
)
> :not(:first-child) {
/* drop the leading border so neighbors share one seam, not two */
border-block-start-width: 0;
border-start-start-radius: 0;
border-start-end-radius: 0;
}
}Multiple
<ui-toggle-group role="group" aria-label="Text formatting">
<label data-ui="toggle">
<input type="checkbox" name="format" value="bold" checked />
<span>Bold</span>
</label>
<label data-ui="toggle">
<input type="checkbox" name="format" value="italic" />
<span>Italic</span>
</label>
<label data-ui="toggle">
<input type="checkbox" name="format" value="underline" checked />
<span>Underline</span>
</label>
</ui-toggle-group>/**
* toggle-group.css: Toggle group (ui-toggle-group | [data-ui~="toggle-group"])
*
* @layer variables, components
* @requires _layers.css, _variables.css, _properties.css (--_*-resolved), toggle.css
* @uses :not(): first/last edge selection for shared-border collapse
* @uses logical border-radius longhands: RTL-safe corner rounding per orientation
* @uses flex-item z-index: raise the active control so its border/ring isn't clipped
* @tokens --ui-toggle-group-* (@layer variables; named after the utilities they back)
*/
@layer variables {
:root {
--ui-toggle-group-rounded: var(--ui-toggle-rounded);
--ui-toggle-group-bg: var(--color-card);
}
}
@property --_toggle-group-rounded {
syntax: "*";
inherits: false;
}
@property --_toggle-group-bg {
syntax: "*";
inherits: false;
}
@layer ui.components {
/* ===========================================================================
TOGGLE GROUP
Joins adjacent toggles into a single segmented unit: a single-select set
(radios sharing a `name`) or a multi-select set (checkboxes). Children keep
their own --ui-toggle-rounded, so the outer corners follow the toggles' size
preset automatically; only the *inner* corners are squared off and the
doubled border is collapsed by dropping
each later control's leading border (no overlap).
=========================================================================== */
:where(ui-toggle-group, [data-ui~="toggle-group"]) {
display: flex;
inline-size: fit-content;
/* stretch so mixed-height children share one edge */
align-items: stretch;
border-radius: var(
--_rounded-resolved,
var(--_toggle-group-rounded, var(--ui-toggle-group-rounded))
);
background-color: var(--_bg-resolved, var(--_toggle-group-bg, var(--ui-toggle-group-bg)));
position: relative;
isolation: isolate;
}
/* flex items honour z-index without positioning, but relative keeps the
stacking context robust across child types */
:where(ui-toggle-group, [data-ui~="toggle-group"]) > * {
position: relative;
}
/* the focused/hovered/checked toggle floats above its neighbors so its
focus ring paints over its neighbors */
:where(ui-toggle-group, [data-ui~="toggle-group"]) > *:hover:not(:has(> input:disabled)),
:where(ui-toggle-group, [data-ui~="toggle-group"]) > *:focus-within,
:where(ui-toggle-group, [data-ui~="toggle-group"]) > *:has(> input:checked) {
z-index: 1;
}
/* ===========================================================================
TOGGLE GROUP: HORIZONTAL (default)
=========================================================================== */
:where(ui-toggle-group, [data-ui~="toggle-group"]):not(
[data-toggle-group-orientation="vertical"]
) {
flex-direction: row;
}
:where(ui-toggle-group, [data-ui~="toggle-group"]):not([data-toggle-group-orientation="vertical"])
> :not(:last-child) {
border-start-end-radius: 0;
border-end-end-radius: 0;
}
:where(ui-toggle-group, [data-ui~="toggle-group"]):not([data-toggle-group-orientation="vertical"])
> :not(:first-child) {
/* drop the leading border so neighbors share one seam, not two */
border-inline-start-width: 0;
border-start-start-radius: 0;
border-end-start-radius: 0;
}
/* ===========================================================================
TOGGLE GROUP: VERTICAL — data-toggle-group-orientation="vertical"
=========================================================================== */
:where(
ui-toggle-group[data-toggle-group-orientation="vertical"],
[data-ui~="toggle-group"][data-toggle-group-orientation="vertical"]
) {
flex-direction: column;
}
:where(
ui-toggle-group[data-toggle-group-orientation="vertical"],
[data-ui~="toggle-group"][data-toggle-group-orientation="vertical"]
)
> :not(:last-child) {
border-end-start-radius: 0;
border-end-end-radius: 0;
}
:where(
ui-toggle-group[data-toggle-group-orientation="vertical"],
[data-ui~="toggle-group"][data-toggle-group-orientation="vertical"]
)
> :not(:first-child) {
/* drop the leading border so neighbors share one seam, not two */
border-block-start-width: 0;
border-start-start-radius: 0;
border-start-end-radius: 0;
}
}Vertical
<ui-toggle-group data-toggle-group-orientation="vertical" role="group" aria-label="Map layer">
<label data-ui="toggle">
<input type="radio" name="layer" value="map" checked />
<span>Map</span>
</label>
<label data-ui="toggle">
<input type="radio" name="layer" value="satellite" />
<span>Satellite</span>
</label>
<label data-ui="toggle">
<input type="radio" name="layer" value="terrain" />
<span>Terrain</span>
</label>
</ui-toggle-group>/**
* toggle-group.css: Toggle group (ui-toggle-group | [data-ui~="toggle-group"])
*
* @layer variables, components
* @requires _layers.css, _variables.css, _properties.css (--_*-resolved), toggle.css
* @uses :not(): first/last edge selection for shared-border collapse
* @uses logical border-radius longhands: RTL-safe corner rounding per orientation
* @uses flex-item z-index: raise the active control so its border/ring isn't clipped
* @tokens --ui-toggle-group-* (@layer variables; named after the utilities they back)
*/
@layer variables {
:root {
--ui-toggle-group-rounded: var(--ui-toggle-rounded);
--ui-toggle-group-bg: var(--color-card);
}
}
@property --_toggle-group-rounded {
syntax: "*";
inherits: false;
}
@property --_toggle-group-bg {
syntax: "*";
inherits: false;
}
@layer ui.components {
/* ===========================================================================
TOGGLE GROUP
Joins adjacent toggles into a single segmented unit: a single-select set
(radios sharing a `name`) or a multi-select set (checkboxes). Children keep
their own --ui-toggle-rounded, so the outer corners follow the toggles' size
preset automatically; only the *inner* corners are squared off and the
doubled border is collapsed by dropping
each later control's leading border (no overlap).
=========================================================================== */
:where(ui-toggle-group, [data-ui~="toggle-group"]) {
display: flex;
inline-size: fit-content;
/* stretch so mixed-height children share one edge */
align-items: stretch;
border-radius: var(
--_rounded-resolved,
var(--_toggle-group-rounded, var(--ui-toggle-group-rounded))
);
background-color: var(--_bg-resolved, var(--_toggle-group-bg, var(--ui-toggle-group-bg)));
position: relative;
isolation: isolate;
}
/* flex items honour z-index without positioning, but relative keeps the
stacking context robust across child types */
:where(ui-toggle-group, [data-ui~="toggle-group"]) > * {
position: relative;
}
/* the focused/hovered/checked toggle floats above its neighbors so its
focus ring paints over its neighbors */
:where(ui-toggle-group, [data-ui~="toggle-group"]) > *:hover:not(:has(> input:disabled)),
:where(ui-toggle-group, [data-ui~="toggle-group"]) > *:focus-within,
:where(ui-toggle-group, [data-ui~="toggle-group"]) > *:has(> input:checked) {
z-index: 1;
}
/* ===========================================================================
TOGGLE GROUP: HORIZONTAL (default)
=========================================================================== */
:where(ui-toggle-group, [data-ui~="toggle-group"]):not(
[data-toggle-group-orientation="vertical"]
) {
flex-direction: row;
}
:where(ui-toggle-group, [data-ui~="toggle-group"]):not([data-toggle-group-orientation="vertical"])
> :not(:last-child) {
border-start-end-radius: 0;
border-end-end-radius: 0;
}
:where(ui-toggle-group, [data-ui~="toggle-group"]):not([data-toggle-group-orientation="vertical"])
> :not(:first-child) {
/* drop the leading border so neighbors share one seam, not two */
border-inline-start-width: 0;
border-start-start-radius: 0;
border-end-start-radius: 0;
}
/* ===========================================================================
TOGGLE GROUP: VERTICAL — data-toggle-group-orientation="vertical"
=========================================================================== */
:where(
ui-toggle-group[data-toggle-group-orientation="vertical"],
[data-ui~="toggle-group"][data-toggle-group-orientation="vertical"]
) {
flex-direction: column;
}
:where(
ui-toggle-group[data-toggle-group-orientation="vertical"],
[data-ui~="toggle-group"][data-toggle-group-orientation="vertical"]
)
> :not(:last-child) {
border-end-start-radius: 0;
border-end-end-radius: 0;
}
:where(
ui-toggle-group[data-toggle-group-orientation="vertical"],
[data-ui~="toggle-group"][data-toggle-group-orientation="vertical"]
)
> :not(:first-child) {
/* drop the leading border so neighbors share one seam, not two */
border-block-start-width: 0;
border-start-start-radius: 0;
border-start-end-radius: 0;
}
}Attributes
| Attribute | Kind | Values | Notes |
|---|---|---|---|
data-ui="toggle-group" | Identity | free value | Identity token in data-ui. |
<ui-toggle-group> | Tag form | free value | Tag form; styled and, when scripted, registered under this name. |
data-toggle-group-orientation | Preset | vertical | Preset: a named bundle of values. |
Requires: toggle.
Hooks
Set a hook on :root, on a subtree, or inline on one element. A state hook (--…--hover) themes that state only; the primitive keeps its other states.
| Hook | Default | State |
|---|---|---|
--ui-toggle-group-bg | var(--color-card) | — |
--ui-toggle-group-rounded | var(--ui-toggle-rounded) | — |