# Button Group

A container that groups related buttons with shared borders and outer corners only.

```tsx
import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"
import { Archive, Clock, MailOpen, Trash2 } from "lucide-react"

export default function ButtonGroupDemo() {
  return (
    <ButtonGroup aria-label="Message actions">
      <Button variant="outline" size="icon" aria-label="Archive">
        <Archive />
      </Button>
      <Button variant="outline" size="icon" aria-label="Mark as read">
        <MailOpen />
      </Button>
      <Button variant="outline" size="icon" aria-label="Snooze">
        <Clock />
      </Button>
      <Button variant="outline" size="icon" aria-label="Delete">
        <Trash2 />
      </Button>
    </ButtonGroup>
  )
}
```

## Installation

1. Install the following dependencies:

   ```bash
   npm install @base-ui/react @stylexjs/stylex clsx
   ```

2. Set up the token layer and the StyleX compiler, if you have not already. See
   [Installation](https://carbon.style/docs/installation). `ButtonGroup` also needs the
   [Button](https://carbon.style/docs/components/button), which it imports `ButtonGroupContext` and
   `buttonStyles` from.

3. Copy and paste the following code into your project.

   ```tsx title="components/ui/button-group.tsx"
   "use client"

   import { use } from "react"
   import { Separator as SeparatorPrimitive } from "@base-ui/react/separator"
   import { useRender } from "@base-ui/react/use-render"
   import * as stylex from "@stylexjs/stylex"
   import type { StyleXStyles } from "@stylexjs/stylex"
   import { clsx } from "clsx"

   import { ButtonGroupContext, buttonStyles, type ButtonGroupOrientation } from "@/components/ui/button"
   import { color, leading, radius, space, text } from "@/theme/tokens.stylex"

   const styles = stylex.create({
     group: { display: "flex", width: "fit-content", alignItems: "stretch" },
     vertical: { flexDirection: "column" },
     text: {
       display: "flex",
       alignItems: "center",
       gap: space.s2,
       borderRadius: "var(--carbon-button-radius)",
       borderWidth: 1,
       borderStyle: "solid",
       backgroundColor: color.muted,
       paddingInline: space.s2_5,
       fontSize: text.sm,
       lineHeight: leading.sm,
       fontWeight: 500,
       "--carbon-icon-size": space.s4,
       "--carbon-button-radius": radius.lg,
     },
     separator: {
       position: "relative",
       alignSelf: "stretch",
       flexShrink: 0,
       backgroundColor: color.buttonSecondaryEdge,
     },
     separatorVertical: { width: 1, marginInlineStart: -1 },
     separatorHorizontal: { height: 1, marginBlockStart: -1 },
   })

   type ButtonGroupProps = useRender.ComponentProps<"div"> & {
     orientation?: ButtonGroupOrientation
     xstyle?: StyleXStyles
   }

   function ButtonGroup({
     render,
     ref,
     style,
     orientation = "horizontal",
     xstyle,
     className,
     children,
     ...props
   }: ButtonGroupProps) {
     const sx = stylex.props(styles.group, orientation === "vertical" && styles.vertical, xstyle)

     return (
       <ButtonGroupContext value={{ orientation }}>
         {useRender({
           defaultTagName: "div",
           render,
           ref,
           props: {
             role: "group",
             "data-slot": "button-group",
             "data-orientation": orientation,
             ...sx,
             ...props,
             className: clsx(sx.className, className),
             style: { ...sx.style, ...style },
             children,
           },
         })}
       </ButtonGroupContext>
     )
   }

   type ButtonGroupTextProps = useRender.ComponentProps<"div"> & {
     xstyle?: StyleXStyles
   }

   function ButtonGroupText({ xstyle, className, render, ...props }: ButtonGroupTextProps) {
     const grouped = use(ButtonGroupContext)
     const sx = stylex.props(styles.text, grouped && buttonStyles.group[grouped.orientation], xstyle)

     return useRender({
       defaultTagName: "div",
       render,
       props: {
         "data-slot": "button-group-text",
         ...sx,
         ...props,
         className: clsx("carbon-icons", sx.className, className),
       },
     })
   }

   type ButtonGroupSeparatorProps = Omit<SeparatorPrimitive.Props, "className"> & {
     xstyle?: StyleXStyles
     className?: string
   }

   function ButtonGroupSeparator({
     orientation,
     xstyle,
     className,
     ...props
   }: ButtonGroupSeparatorProps) {
     const grouped = use(ButtonGroupContext)
     const resolved = orientation ?? (grouped?.orientation === "vertical" ? "horizontal" : "vertical")
     const sx = stylex.props(
       styles.separator,
       resolved === "vertical" ? styles.separatorVertical : styles.separatorHorizontal,
       xstyle,
     )

     return (
       <SeparatorPrimitive
         data-slot="button-group-separator"
         orientation={resolved}
         {...sx}
         {...props}
         className={clsx(sx.className, className)}
       />
     )
   }

   export { ButtonGroup, ButtonGroupSeparator, ButtonGroupText }
   ```

4. Update the import paths to match your project setup.

## Usage

```tsx
import { Button } from "@/components/ui/button"
import { ButtonGroup, ButtonGroupSeparator, ButtonGroupText } from "@/components/ui/button-group"
```

```tsx
<ButtonGroup aria-label="Message actions">
  <Button variant="outline">Archive</Button>
  <Button variant="outline">Report</Button>
  <Button variant="outline">Snooze</Button>
</ButtonGroup>
```

Give every group an `aria-label` or `aria-labelledby`. Without one, a screen
reader announces the group with no name.

Use `ButtonGroup` for buttons that each perform an action. Buttons that toggle a
shared state are a toggle group, which carbon does not ship yet.

## Orientation

Set `orientation="vertical"` to stack the members.

```tsx
import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"
import { Minus, Plus } from "lucide-react"

export default function ButtonGroupOrientation() {
  return (
    <ButtonGroup orientation="vertical" aria-label="Zoom">
      <Button variant="outline" size="icon" aria-label="Zoom in">
        <Plus />
      </Button>
      <Button variant="outline" size="icon" aria-label="Zoom out">
        <Minus />
      </Button>
    </ButtonGroup>
  )
}
```

## Size

The group takes no `size`. Set it on each button, and give every member the
same one.

```tsx
import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"
import { VStack } from "@/components/ui/stack"

export default function ButtonGroupSize() {
  return (
    <VStack gap={4} align="start">
      <ButtonGroup aria-label="Small">
        <Button variant="outline" size="sm">
          Day
        </Button>
        <Button variant="outline" size="sm">
          Week
        </Button>
        <Button variant="outline" size="sm">
          Month
        </Button>
      </ButtonGroup>
      <ButtonGroup aria-label="Default">
        <Button variant="outline">Day</Button>
        <Button variant="outline">Week</Button>
        <Button variant="outline">Month</Button>
      </ButtonGroup>
      <ButtonGroup aria-label="Large">
        <Button variant="outline" size="lg">
          Day
        </Button>
        <Button variant="outline" size="lg">
          Week
        </Button>
        <Button variant="outline" size="lg">
          Month
        </Button>
      </ButtonGroup>
    </VStack>
  )
}
```

## Side by side

Nesting a group inside another group does nothing. To place groups side by
side, put them in an `HStack` and set the space between them with its `gap`.

```tsx
import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"
import { HStack } from "@/components/ui/stack"
import { ChevronLeft, ChevronRight } from "lucide-react"

export default function ButtonGroupNested() {
  return (
    <HStack gap={2}>
      <ButtonGroup aria-label="Pages">
        <Button variant="outline">1</Button>
        <Button variant="outline">2</Button>
        <Button variant="outline">3</Button>
      </ButtonGroup>
      <ButtonGroup aria-label="Pagination">
        <Button variant="outline" size="icon" aria-label="Previous">
          <ChevronLeft />
        </Button>
        <Button variant="outline" size="icon" aria-label="Next">
          <ChevronRight />
        </Button>
      </ButtonGroup>
    </HStack>
  )
}
```

## Separator

Use `ButtonGroupSeparator` between solid variants, which have no visible
border of their own. It runs across the group's axis.

```tsx
import { Button } from "@/components/ui/button"
import { ButtonGroup, ButtonGroupSeparator } from "@/components/ui/button-group"

export default function ButtonGroupSeparatorDemo() {
  return (
    <ButtonGroup aria-label="Text alignment">
      <Button variant="secondary">Left</Button>
      <ButtonGroupSeparator />
      <Button variant="secondary">Center</Button>
      <ButtonGroupSeparator />
      <Button variant="secondary">Right</Button>
    </ButtonGroup>
  )
}
```

> **Note**
>
> Outline buttons already share their border, so a separator between them draws a
> second line. Use it with `default` and `secondary`.

## Split button

Pair an action with a menu that offers the alternatives. The menu trigger
renders a `Button`, so it joins the group like any other member.

```tsx
import { Button } from "@/components/ui/button"
import { ButtonGroup, ButtonGroupSeparator } from "@/components/ui/button-group"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { ChevronDown } from "lucide-react"

export default function ButtonGroupSplit() {
  return (
    <ButtonGroup aria-label="Merge">
      <Button variant="secondary">Merge pull request</Button>
      <ButtonGroupSeparator />
      <DropdownMenu>
        <DropdownMenuTrigger
          render={<Button variant="secondary" size="icon" aria-label="More merge options" />}
        >
          <ChevronDown />
        </DropdownMenuTrigger>
        <DropdownMenuContent align="end">
          <DropdownMenuItem>Squash and merge</DropdownMenuItem>
          <DropdownMenuItem>Rebase and merge</DropdownMenuItem>
          <DropdownMenuItem>Create a merge commit</DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
    </ButtonGroup>
  )
}
```

## With text

Use `ButtonGroupText` for an inert member, such as a label or a unit. Pass
`render` to change the element.

```tsx
import { Button } from "@/components/ui/button"
import { ButtonGroup, ButtonGroupText } from "@/components/ui/button-group"
import { Copy } from "lucide-react"

export default function ButtonGroupTextDemo() {
  return (
    <ButtonGroup aria-label="Install command">
      <ButtonGroupText render={<code />}>npm install @base-ui/react</ButtonGroupText>
      <Button variant="outline" size="icon" aria-label="Copy command">
        <Copy />
      </Button>
    </ButtonGroup>
  )
}
```

## API reference

Pass `render` to replace a part's element or compose it with another component.

### ButtonGroup

Renders a `div` with `role="group"`. A member is any child carrying a
`data-slot`.

| Prop          | Type                         | Default        |
| ------------- | ---------------------------- | -------------- |
| `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` |
| `xstyle`      | `StyleXStyles`               | —              |
| `className`   | `string`                     | —              |

### ButtonGroupSeparator

Wraps Base UI's `Separator`.

| Prop          | Type                         | Default                 |
| ------------- | ---------------------------- | ----------------------- |
| `orientation` | `"horizontal" \| "vertical"` | across the group's axis |
| `xstyle`      | `StyleXStyles`               | —                       |
| `className`   | `string`                     | —                       |

### ButtonGroupText

Renders a `div` through Base UI's `useRender`, so `render` swaps the element.

| Prop        | Type           | Default |
| ----------- | -------------- | ------- |
| `render`    | `ReactElement` | —       |
| `xstyle`    | `StyleXStyles` | —       |
| `className` | `string`       | —       |

### Data attributes

| Attribute                            | On            | Effect                          |
| ------------------------------------ | ------------- | ------------------------------- |
| `data-slot="button-group"`           | the group     | Targets the group from a parent |
| `data-orientation`                   | the group     | Reflects the orientation prop   |
| `data-slot="button-group-separator"` | the separator | Targets the separator           |
| `data-slot="button-group-text"`      | the text      | Targets the text member         |
