API
Button group
Buttons joined into one control with shared borders.
Installation
pnpm dlx zazz-ui add button-groupnpx zazz-ui add button-groupyarn dlx zazz-ui add button-groupbunx zazz-ui add button-groupAlso adds fields, kbd, button, which it depends on. Run zazz-ui init first if the project has no zazz/ folder yet.
Examples
Default
<div
style="
--display: flex-col;
--items: start;
--gap: 6;
"
>
<ui-button-group role="group" aria-label="Text alignment">
<button data-ui="button" type="button">Left</button>
<button data-ui="button" type="button">Center</button>
<button data-ui="button" type="button">Right</button>
</ui-button-group>
<ui-button-group role="group" aria-label="Pagination">
<button data-ui="button" data-button-variant="muted" type="button">Previous</button>
<button data-ui="button" data-button-variant="muted" type="button">1</button>
<button data-ui="button" data-button-variant="muted" type="button">2</button>
<button data-ui="button" data-button-variant="muted" type="button">3</button>
<button data-ui="button" data-button-variant="muted" type="button">Next</button>
</ui-button-group>
<ui-button-group role="group" aria-label="Formatting">
<button data-ui="button" data-button-size="icon" type="button" aria-label="Bold">
<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"
>
<path d="M6 4h8a4 4 0 0 1 0 8H6z" />
<path d="M6 12h9a4 4 0 0 1 0 8H6z" />
</svg>
</button>
<button data-ui="button" data-button-size="icon" type="button" aria-label="Italic">
<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="19" y1="4" x2="10" y2="4" />
<line x1="14" y1="20" x2="5" y2="20" />
<line x1="15" y1="4" x2="9" y2="20" />
</svg>
</button>
<button data-ui="button" data-button-size="icon" type="button" aria-label="Underline">
<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"
>
<path d="M6 4v6a6 6 0 0 0 12 0V4" />
<line x1="4" y1="20" x2="20" y2="20" />
</svg>
</button>
</ui-button-group>
</div>/**
* button-group.css: Button group (ui-button-group | [data-ui~="button-group"])
*
* @layer variables, components
* @requires _layers.css, _variables.css, _properties.css (--_*-resolved), button.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-button-group-* (@layer variables; named after the utilities they back)
*/
@layer variables {
:root {
--ui-button-group-rounded: var(--ui-button-rounded);
--ui-button-group-bg: var(--color-card);
}
}
@property --_button-group-rounded {
syntax: "*";
inherits: false;
}
@property --_button-group-bg {
syntax: "*";
inherits: false;
}
@layer ui.components {
/* ===========================================================================
BUTTON GROUP
Joins adjacent controls (buttons, and any child it wraps) into a single
segmented unit. Children keep their own --ui-button-rounded, so the outer corners
follow the buttons' 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-button-group, [data-ui~="button-group"]) {
display: flex;
inline-size: fit-content;
/* stretch so mixed-height children share one edge */
align-items: stretch;
border-radius: var(
--_rounded-resolved,
var(--_button-group-rounded, var(--ui-button-group-rounded))
);
background-color: var(--_bg-resolved, var(--_button-group-bg, var(--ui-button-group-bg)));
position: relative;
isolation: isolate;
}
/* flex items honour z-index without positioning, but relative keeps the
stacking context robust across child types (inputs, selects, …) */
:where(ui-button-group, [data-ui~="button-group"]) > * {
position: relative;
}
/* the focused/hovered/pressed control floats above its neighbors so its
focus ring paints over its neighbors */
:where(ui-button-group, [data-ui~="button-group"])
> *:hover:not(:disabled, [aria-disabled="true"]),
:where(ui-button-group, [data-ui~="button-group"]) > *:focus-visible,
:where(ui-button-group, [data-ui~="button-group"])
> *:active:not(:disabled, [aria-disabled="true"]) {
z-index: 1;
}
/* ===========================================================================
BUTTON GROUP: HORIZONTAL (default)
=========================================================================== */
:where(ui-button-group, [data-ui~="button-group"]):not(
[data-button-group-orientation="vertical"]
) {
flex-direction: row;
}
:where(ui-button-group, [data-ui~="button-group"]):not([data-button-group-orientation="vertical"])
> :not(:last-child) {
border-start-end-radius: 0;
border-end-end-radius: 0;
}
:where(ui-button-group, [data-ui~="button-group"]):not([data-button-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;
}
/* ===========================================================================
BUTTON GROUP: VERTICAL — data-button-group-orientation="vertical"
=========================================================================== */
:where(
ui-button-group[data-button-group-orientation="vertical"],
[data-ui~="button-group"][data-button-group-orientation="vertical"]
) {
flex-direction: column;
}
:where(
ui-button-group[data-button-group-orientation="vertical"],
[data-ui~="button-group"][data-button-group-orientation="vertical"]
)
> :not(:last-child) {
border-end-start-radius: 0;
border-end-end-radius: 0;
}
:where(
ui-button-group[data-button-group-orientation="vertical"],
[data-ui~="button-group"][data-button-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-button-group data-button-group-orientation="vertical" role="group" aria-label="View options">
<button data-ui="button" type="button">List</button>
<button data-ui="button" type="button">Board</button>
<button data-ui="button" type="button">Calendar</button>
<button data-ui="button" type="button">Timeline</button>
</ui-button-group>/**
* button-group.css: Button group (ui-button-group | [data-ui~="button-group"])
*
* @layer variables, components
* @requires _layers.css, _variables.css, _properties.css (--_*-resolved), button.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-button-group-* (@layer variables; named after the utilities they back)
*/
@layer variables {
:root {
--ui-button-group-rounded: var(--ui-button-rounded);
--ui-button-group-bg: var(--color-card);
}
}
@property --_button-group-rounded {
syntax: "*";
inherits: false;
}
@property --_button-group-bg {
syntax: "*";
inherits: false;
}
@layer ui.components {
/* ===========================================================================
BUTTON GROUP
Joins adjacent controls (buttons, and any child it wraps) into a single
segmented unit. Children keep their own --ui-button-rounded, so the outer corners
follow the buttons' 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-button-group, [data-ui~="button-group"]) {
display: flex;
inline-size: fit-content;
/* stretch so mixed-height children share one edge */
align-items: stretch;
border-radius: var(
--_rounded-resolved,
var(--_button-group-rounded, var(--ui-button-group-rounded))
);
background-color: var(--_bg-resolved, var(--_button-group-bg, var(--ui-button-group-bg)));
position: relative;
isolation: isolate;
}
/* flex items honour z-index without positioning, but relative keeps the
stacking context robust across child types (inputs, selects, …) */
:where(ui-button-group, [data-ui~="button-group"]) > * {
position: relative;
}
/* the focused/hovered/pressed control floats above its neighbors so its
focus ring paints over its neighbors */
:where(ui-button-group, [data-ui~="button-group"])
> *:hover:not(:disabled, [aria-disabled="true"]),
:where(ui-button-group, [data-ui~="button-group"]) > *:focus-visible,
:where(ui-button-group, [data-ui~="button-group"])
> *:active:not(:disabled, [aria-disabled="true"]) {
z-index: 1;
}
/* ===========================================================================
BUTTON GROUP: HORIZONTAL (default)
=========================================================================== */
:where(ui-button-group, [data-ui~="button-group"]):not(
[data-button-group-orientation="vertical"]
) {
flex-direction: row;
}
:where(ui-button-group, [data-ui~="button-group"]):not([data-button-group-orientation="vertical"])
> :not(:last-child) {
border-start-end-radius: 0;
border-end-end-radius: 0;
}
:where(ui-button-group, [data-ui~="button-group"]):not([data-button-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;
}
/* ===========================================================================
BUTTON GROUP: VERTICAL — data-button-group-orientation="vertical"
=========================================================================== */
:where(
ui-button-group[data-button-group-orientation="vertical"],
[data-ui~="button-group"][data-button-group-orientation="vertical"]
) {
flex-direction: column;
}
:where(
ui-button-group[data-button-group-orientation="vertical"],
[data-ui~="button-group"][data-button-group-orientation="vertical"]
)
> :not(:last-child) {
border-end-start-radius: 0;
border-end-end-radius: 0;
}
:where(
ui-button-group[data-button-group-orientation="vertical"],
[data-ui~="button-group"][data-button-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="button-group" | Identity | free value | Identity token in data-ui. |
<ui-button-group> | Tag form | free value | Tag form; styled and, when scripted, registered under this name. |
data-button-group-orientation | Preset | vertical | Preset: a named bundle of values. |
Requires: button.
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-button-group-bg | var(--color-card) | — |
--ui-button-group-rounded | var(--ui-button-rounded) | — |