Button-group
Filter buttons
Toggle row (text formatting)
Primary action set
Vertical
/*
* Button-group — element (interactive)
*
* Joins multiple Buttons or Toggles into a connected horizontal row
* with shared borders and squared inner corners. CSS-only — relies on
* sibling selectors.
*
* <div class="button-group" role="group" aria-label="Filter by status">
* <button class="button -secondary">All</button>
* <button class="button -secondary">Active</button>
* <button class="button -secondary">Archived</button>
* </div>
*
* <div class="button-group">
* <button class="toggle" aria-pressed="false">B</button>
* <button class="toggle" aria-pressed="true">I</button>
* <button class="toggle" aria-pressed="false">U</button>
* </div>
*/
@layer malevich.components {
.button-group {
display: inline-flex;
align-items: stretch;
isolation: isolate;
}
/* Strip rounded corners between siblings. The first child keeps
* inline-start radius; the last keeps inline-end. Middle children
* lose all radius.
*/
.button-group > .button,
.button-group > .toggle {
border-radius: 0;
}
.button-group > .button:first-child,
.button-group > .toggle:first-child {
border-start-start-radius: var(--radius-button);
border-end-start-radius: var(--radius-button);
}
.button-group > .button:last-child,
.button-group > .toggle:last-child {
border-start-end-radius: var(--radius-button);
border-end-end-radius: var(--radius-button);
}
/* Collapse the duplicate inner border between adjacent siblings. */
.button-group > .button + .button,
.button-group > .button + .toggle,
.button-group > .toggle + .button,
.button-group > .toggle + .toggle {
margin-inline-start: calc(-1 * var(--border-width-hairline));
}
/* Focused / pressed / hovered child rises above its neighbors. */
.button-group > .button:hover,
.button-group > .button:focus-visible,
.button-group > .toggle:hover,
.button-group > .toggle:focus-visible,
.button-group > .toggle[aria-pressed="true"] {
z-index: 1;
}
/* Variants */
.button-group.-vertical {
flex-direction: column;
}
.button-group.-vertical > .button:first-child,
.button-group.-vertical > .toggle:first-child {
border-start-start-radius: var(--radius-button);
border-start-end-radius: var(--radius-button);
border-end-start-radius: 0;
border-end-end-radius: 0;
}
.button-group.-vertical > .button:last-child,
.button-group.-vertical > .toggle:last-child {
border-start-start-radius: 0;
border-start-end-radius: 0;
border-end-start-radius: var(--radius-button);
border-end-end-radius: var(--radius-button);
}
.button-group.-vertical > .button + .button,
.button-group.-vertical > .button + .toggle,
.button-group.-vertical > .toggle + .button,
.button-group.-vertical > .toggle + .toggle {
margin-inline-start: 0;
margin-block-start: calc(-1 * var(--border-width-hairline));
}
}
This component is pure CSS — no JavaScript required.
# Button-Group — AGENTS.md
> Auto-generated from `button-group.docs.md` + the modifier applicability matrix.
> Edit those source files; this file regenerates on build.
## Identity
- **Component:** `button-group`
- **Layer:** elements
- **Status:** stable
- **Last updated:** 2026-05-19
## Summary
- A small set of mutually-related actions or filters.
## Variants
| Variant | Class | Use for |
|-----------|-----------------------------|----------------------------------------|
| Default | `.button-group` | Horizontal row |
| Vertical | `.button-group.-vertical` | Stacked column |
## Modifier applicability
Per ADR-0007. Modifiers attach via `data-{category}="value"` attributes.
| Category | Accepts |
|---|---|
| `background` | — |
| `surface` | `flat` |
| `effect` | — |
| `shader` | — |
| `rhythm` | `compact`, `regular` |
| `motion` | any value |
| `behavior` | — |
## Anatomy
```html
<!-- Group of buttons -->
<div class="button-group" role="group" aria-label="Filter by status">
<button class="button -secondary">All</button>
<button class="button -secondary">Active</button>
<button class="button -secondary">Archived</button>
</div>
<!-- Group of toggles (multi-select chips) -->
<div class="button-group" role="group" aria-label="Text formatting">
<button class="toggle" aria-pressed="false">B</button>
<button class="toggle" aria-pressed="true">I</button>
<button class="toggle" aria-pressed="false">U</button>
</div>
<!-- Vertical -->
<div class="button-group -vertical" role="group" aria-label="View">
<button class="button -secondary">List</button>
<button class="button -secondary">Grid</button>
<button class="button -secondary">Cards</button>
</div>
```
The component is purely a layout wrapper — children remain real
`<button>` elements with all their semantics.
## Tokens consumed
- `--radius-button` — outer corner radius
- `--border-width-hairline` — used for negative-margin overlap
## Accessibility contract
- Always wrap in `role="group"` with `aria-label` so screen readers
announce the group's purpose.
- Children remain real `<button>` elements — keyboard, focus, and
press semantics are unchanged.
- The group does not implement roving tab index. Tab/Shift+Tab moves
through each button individually (matching native button behavior).
If you want arrow-key navigation, use `Tabs` instead.
## Guidance
### Do
- Use real `<button>` children, not styled divs.
- Provide `role="group"` and `aria-label`.
- Keep the group size small — 2-5 items is the sweet spot.
### Don't
- Don't nest Button-groups.
- Don't mix sizes (button.-s + button.-l) within one group. Heights
won't align.