<!-- Protocore Design System — ButtonGroup -->
# ButtonGroup

- **Category:** Inputs (`inputs`)
- **Slug:** `inputs/button-group`
- **Status:** stable
- **Platforms:** web
- **Import:** `import { ButtonGroup } from "@protocore/pds";`
- **Docs:** https://pds.protocore.io/components/inputs/button-group

> A horizontal cluster of Buttons or IconButtons — seamed into one control by default, or spaced by a gap.

## When to use

Use **ButtonGroup** to visually associate a small set of *independent* actions — each member still fires its own `onClick`.

- Choosing **one** option from a mutually-exclusive set (a view mode, a time range)? That's a selection, not an action — use **SegmentedControl** or **RadioGroup** so the pressed state is modelled correctly.
- Paging through results? Use **Pagination**, which handles the numbering and boundaries for you.
- A group is a layout wrapper, not a roving-focus widget: every button inside stays independently tabbable.

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `attached` | `boolean` | no | `true` | Butt the buttons together and collapse their shared inner borders into one seam. Defaults to `true`. |
| `className` | `string` | no | — | — |
| `gap` | `enum` | no | `sm` | Space between members when not `attached`. Ignored while `attached`. |
| `style` | `CSSProperties` | no | — | — |

## Examples

### Basics

By default the group is `attached`: the members butt together and their shared inner borders collapse into a single seam, reading as one segmented control. Pass an `aria-label` to name the cluster.

```tsx
import { Button, ButtonGroup } from "@protocore/pds";

export default function Basics() {
  // Attached by default: the inner borders collapse into a single seam.
  return (
    <ButtonGroup aria-label="Ledger actions">
      <Button variant="secondary">Copy</Button>
      <Button variant="secondary">Export</Button>
      <Button variant="secondary">Archive</Button>
    </ButtonGroup>
  );
}
```

### Detached

Set `attached={false}` for related-but-separate actions — a primary/secondary CTA pair, say. The `gap` prop (`sm` · `md` · `lg`) controls the spacing.

```tsx
import { Button, ButtonGroup } from "@protocore/pds";

export default function Detached() {
  // attached={false} spaces members by a gap variant instead of seaming them.
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
      <ButtonGroup attached={false} gap="sm" aria-label="Draft actions">
        <Button variant="ghost">Discard</Button>
        <Button>Save draft</Button>
      </ButtonGroup>
      <ButtonGroup attached={false} gap="lg" aria-label="Publish actions">
        <Button variant="ghost">Cancel</Button>
        <Button variant="primary">Publish</Button>
      </ButtonGroup>
    </div>
  );
}
```

## Do & don't

**Do**

- Group actions that genuinely belong together — copy / export / archive on one record.
- Give the group an aria-label describing the set ("Row actions").
- Keep the members the same variant and size so the seam reads cleanly.
- Use attached={false} with a gap when a CTA pair shouldn't look like one control.

**Don't**

- Don't use it to pick one option from a set — reach for SegmentedControl or RadioGroup.
- Don't mix variants inside an attached group; the seam and heights will clash.
- Don't cram unrelated actions together just because they're adjacent.
- Don't nest ButtonGroups — flatten to a single row instead.

## Accessibility

**Notes**

- Renders a `role="group"` container; pass `aria-label` (or `aria-labelledby`) to name it.
- Each child button keeps its own focus and activation — Tab moves between members.
- For single-select semantics (aria-pressed / roving focus) use SegmentedControl or RadioGroup instead.
- Visual seaming is purely CSS; it doesn't change the members' roles or tab order.

## Related

`button`, `icon-button`, `segmented-control`, `pagination`

---

© Protocore. All rights reserved. Use of the Protocore Design System requires prior written authorization from Protocore (contact@protocore.io). These machine-readable materials must not be ingested into ML-training datasets or derivative design systems. See https://pds.protocore.io/legal/
