---
title: ButtonGroup
description: Join related buttons into one visual control set.
package: moraine
version: 0.6.0
repository: https://github.com/subframe7536/moraine
---

# ButtonGroup

> Join related buttons into one visual control set.

Use ButtonGroup when adjacent actions should read as one control set. Each child remains its own interactive control; the group supplies layout and shared presentation.

## Basic usage

```tsx
import { Button, ButtonGroup } from 'moraine'

export function Example() {
  return (
    <ButtonGroup>
      <Button>Copy</Button>
      <Button variant="outline">Share</Button>
    </ButtonGroup>
  )
}
```

## Anatomy

```text
ButtonGroup [component; slot=root]
├── ButtonGroup.Separator [part; slot=separator]
└── children [internal]
```

The group owns its separators; the caller supplies the child Buttons and their content.

## Usage

### Composition

Use `ButtonGroup` to align related actions without changing their semantics. Add `ButtonGroup.Separator` where a divider helps; it accepts direct `class` and `style`. Root `classes` and `styles` set defaults for the `root` and `separator` slots. Compose a menu or popover trigger as a child when an action needs an overlay.

```tsx
import { Button, ButtonGroup } from 'moraine'

export function GroupComposition() {
  return (
    <div class="flex flex-col gap-4 items-start">
      <ButtonGroup>
        <Button variant="outline">Copy link</Button>
        <Button variant="outline">Duplicate</Button>
        <Button variant="outline">Archive</Button>
      </ButtonGroup>
    </div>
  )
}
```

## Examples

### Toggle buttons

Compose ToggleButton children for independently enabled actions. They inherit the group's size and unpressed variant; `activeVariant` controls each button's pressed appearance. ButtonGroup supplies layout and does not coordinate selection or add toolbar keyboard navigation.

```tsx
import { ButtonGroup, ToggleButton } from 'moraine'

export function ToggleButtons() {
  return (
    <ButtonGroup size="sm" aria-label="Text formatting">
      <ToggleButton leading="i-lucide:bold" defaultPressed>
        Bold
      </ToggleButton>
      <ToggleButton leading="i-lucide:italic">Italic</ToggleButton>
      <ToggleButton leading="i-lucide:underline">Underline</ToggleButton>
    </ButtonGroup>
  )
}
```

### Separator

Insert `ButtonGroup.Separator` only where an explicit boundary is useful. Its orientation follows the group axis: vertical in a horizontal group and horizontal in a vertical group. An explicit `orientation` overrides this default.

```tsx
import { Button, ButtonGroup } from 'moraine'

export function SeparatorExample() {
  return (
    <ButtonGroup aria-label="Clipboard actions">
      <Button variant="secondary">Copy</Button>
      <ButtonGroup.Separator />
      <Button variant="secondary">Paste</Button>
    </ButtonGroup>
  )
}
```

### Dropdown menu composition

```tsx
import { Button, ButtonGroup, DropdownMenu, Icon } from 'moraine'
import type { DropdownMenuT } from 'moraine'
import { createSignal } from 'solid-js'

export function DropdownAction() {
  const [exportedFormat, setExportedFormat] = createSignal<string>()

  const exportItems: DropdownMenuT.Item[] = [
    {
      type: 'group',
      label: 'Export report',
      children: [
        {
          label: 'PDF document',
          description: 'Best for sharing and printing',
          icon: 'i-lucide:file-text',
          onSelect: () => setExportedFormat('PDF'),
        },
        {
          label: 'CSV spreadsheet',
          description: 'Best for analysis and imports',
          icon: 'i-lucide:table-2',
          onSelect: () => setExportedFormat('CSV'),
        },
        {
          label: 'JSON data',
          description: 'Best for integrations',
          icon: 'i-lucide:braces',
          onSelect: () => setExportedFormat('JSON'),
        },
      ],
    },
  ]

  return (
    <div class="flex flex-col gap-3 items-start">
      <ButtonGroup>
        <Button leading="i-lucide:download">Export report</Button>
        <ButtonGroup.Separator />
        <DropdownMenu placement="bottom" align="end">
          <DropdownMenu.Trigger as={Button} size="icon-md">
            <Icon name="i-lucide:chevron-down" />
          </DropdownMenu.Trigger>
          <DropdownMenu.Content items={exportItems} />
        </DropdownMenu>
      </ButtonGroup>

      <p class="text-sm text-muted-foreground min-h-5" role="status" aria-live="polite">
        {exportedFormat() ? `Report exported as ${exportedFormat()}.` : 'Choose an export format.'}
      </p>
    </div>
  )
}
```

### Popover composition

```tsx
import { Button, ButtonGroup, Icon, Popover } from 'moraine'

export function ButtonPopover() {
  return (
    <ButtonGroup aria-label="Document actions">
      <Button leading="i-lucide:save">Save document</Button>
      <Popover>
        <Popover.Trigger as={Button} size="icon-md" aria-label="Open save options">
          <Icon name="i-lucide:chevron-down" />
        </Popover.Trigger>
        <Popover.Content>
          <div class="p-3 space-y-1">
            <p class="text-sm font-medium">Save options</p>
            <p class="text-xs text-muted-foreground">Choose where to save this document.</p>
          </div>
        </Popover.Content>
      </Popover>
    </ButtonGroup>
  )
}
```

### Vertical layout

```tsx
import { Button, ButtonGroup } from 'moraine'

export function Vertical() {
  return (
    <div class="flex flex-wrap gap-6 items-start">
      <ButtonGroup variant="outline" aria-label="Horizontal quantity controls">
        <Button size="icon-md" leading="i-lucide:minus" aria-label="Decrease quantity" />
        <Button size="icon-md" leading="i-lucide:plus" aria-label="Increase quantity" />
      </ButtonGroup>

      <ButtonGroup orientation="vertical" variant="outline" aria-label="Vertical quantity controls">
        <Button size="icon-md" leading="i-lucide:plus" aria-label="Increase quantity" />
        <ButtonGroup.Separator />
        <Button size="icon-md" leading="i-lucide:minus" aria-label="Decrease quantity" />
      </ButtonGroup>
    </div>
  )
}
```

## Attributes

| Attributes | Slot | Description |
| --- | --- | --- |
| `data-orientation` | `button-group-separator` | Stores the rendered orientation (horizontal or vertical). |

## Props

### ButtonGroup

Props for the ButtonGroup component.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| id | string \| undefined | — | Optional identifier for the group root. |
| orientation | 'horizontal' \| 'vertical' \| undefined | 'horizontal' | Visual layout direction. |
| role | JSX.AriaAttributes['role'] \| undefined | — | ARIA role for the group root. |
| size | 'sm' \| 'md' \| 'lg' \| undefined | 'md' | Shared button size. |
| variant | 'default' \| 'secondary' \| 'outline' \| 'ghost' \| 'link' \| 'destructive' \| undefined | 'default' | Shared button treatment. |
| children | JSX.Element \| undefined | — | Buttons or compatible controls rendered as a cohesive group. |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| classes | Classes \| undefined | — | Family slot class defaults for this instance. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |
| styles | Styles \| undefined | — | Family slot style defaults for this instance. |

### ButtonGroup.Separator

Props for the ButtonGroup.Separator component.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| orientation | 'horizontal' \| 'vertical' \| undefined | — | The orientation of the separator. Defaults to perpendicular to the group axis. |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |
