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.