---
title: Button Group
description: Related controls joined by shared edges.
group: Components
order: 20
---

Bring related actions together in one compact control. Outer corners stay rounded while shared edges remain clean and square.

```tsx
import {
  Button,
  ButtonGroup,
  buttonVariants,
} from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"

export function Example() {
  return (
    <div className="flex flex-wrap items-center gap-2">
      <Button variant="outline" size="icon" aria-label="Back">
        <ArrowLeftIcon />
      </Button>
      <ButtonGroup>
        <Button variant="outline">Archive</Button>
        <Button variant="outline">Report</Button>
      </ButtonGroup>
      <ButtonGroup>
        <Button variant="outline">Snooze</Button>
        <DropdownMenu>
          <DropdownMenuTrigger
            aria-label="More actions"
            className={buttonVariants({ variant: "outline", size: "icon" })}
          >
            <MoreHorizontalIcon />
          </DropdownMenuTrigger>
          <DropdownMenuContent align="end">
            <DropdownMenuItem>
              <CheckIcon />
              Mark as done
            </DropdownMenuItem>
            <DropdownMenuItem>
              <ArchiveIcon />
              Move to archive
            </DropdownMenuItem>
            <DropdownMenuSeparator />
            <DropdownMenuItem>
              <ClockIcon />
              Remind me later
            </DropdownMenuItem>
            <DropdownMenuItem>
              <PinIcon />
              Pin this scene
            </DropdownMenuItem>
            <DropdownMenuItem>
              <ShareIcon />
              Copy share link
            </DropdownMenuItem>
          </DropdownMenuContent>
        </DropdownMenu>
      </ButtonGroup>
    </div>
  )
}
```

## Installation

```bash
npx @arctis-sh/@arctis-sh/ui@latest add button-group
```

`ButtonGroup` ships with `Button` and is also exported from `@arctis-sh/ui/button`.

## Usage

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

```tsx
<ButtonGroup>
  <Button variant="outline">Archive</Button>
  <Button variant="outline">Share</Button>
</ButtonGroup>
```

## Composition

```tree
ButtonGroup
├── Button or Input
├── ButtonGroupSeparator
└── ButtonGroupText
```

## Accessibility

`ButtonGroup` sets `role="group"`. Tab moves between its buttons. Give the group a label with `aria-label` or `aria-labelledby`.

```tsx
<ButtonGroup aria-label="Document actions">
  <Button variant="outline">Archive</Button>
  <Button variant="outline">Share</Button>
</ButtonGroup>
```

## Orientation

Set `orientation` to stack the segments instead of placing them side by side.

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

export function Example() {
  return (
    <ButtonGroup orientation="vertical" aria-label="Volume">
      <Button variant="outline" size="icon" aria-label="Increase">
        <PlusIcon />
      </Button>
      <Button variant="outline" size="icon" aria-label="Decrease">
        <MinusIcon />
      </Button>
    </ButtonGroup>
  )
}
```

## Size

Set `size` on each button to control the group's density.

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

export function Example() {
  return (
    <div className="flex flex-col items-start gap-4">
      <ButtonGroup>
        <Button variant="outline" size="sm">
          Small
        </Button>
        <Button variant="outline" size="sm">
          Button
        </Button>
        <Button variant="outline" size="sm">
          Group
        </Button>
        <Button variant="outline" size="icon-sm" aria-label="Add">
          <PlusIcon />
        </Button>
      </ButtonGroup>
      <ButtonGroup>
        <Button variant="outline">Default</Button>
        <Button variant="outline">Button</Button>
        <Button variant="outline">Group</Button>
        <Button variant="outline" size="icon" aria-label="Add">
          <PlusIcon />
        </Button>
      </ButtonGroup>
      <ButtonGroup>
        <Button variant="outline" size="lg">
          Large
        </Button>
        <Button variant="outline" size="lg">
          Button
        </Button>
        <Button variant="outline" size="lg">
          Group
        </Button>
        <Button variant="outline" size="icon-lg" aria-label="Add">
          <PlusIcon />
        </Button>
      </ButtonGroup>
    </div>
  )
}
```

## Nested

Nest `ButtonGroup` components to arrange related clusters with space between them.

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

export function Example() {
  return (
    <ButtonGroup>
      <ButtonGroup>
        <Button variant="outline">1</Button>
        <Button variant="outline">2</Button>
        <Button variant="outline">3</Button>
      </ButtonGroup>
      <ButtonGroup>
        <Button variant="outline" size="icon" aria-label="Add">
          <PlusIcon />
        </Button>
        <Button variant="outline" size="icon" aria-label="Voice">
          <AudioLinesIcon />
        </Button>
      </ButtonGroup>
    </ButtonGroup>
  )
}
```

## Separator

`ButtonGroupSeparator` adds a stronger division between children. Outline groups usually do not need one.

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

export function Example() {
  return (
    <ButtonGroup>
      <Button variant="secondary">Copy</Button>
      <ButtonGroupSeparator />
      <Button variant="secondary">Paste</Button>
    </ButtonGroup>
  )
}
```

## Split

Pair a primary action with a separate menu trigger.

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

export function Example() {
  return (
    <ButtonGroup>
      <Button variant="secondary">
        <PlusIcon data-icon="inline-start" />
        Button
      </Button>
      <ButtonGroupSeparator />
      <DropdownMenu>
        <DropdownMenuTrigger
          aria-label="More options"
          className={buttonVariants({ variant: "secondary", size: "icon" })}
        >
          <ChevronDownIcon />
        </DropdownMenuTrigger>
        <DropdownMenuContent align="end">
          <DropdownMenuItem>Save</DropdownMenuItem>
          <DropdownMenuItem>Save as draft</DropdownMenuItem>
          <DropdownMenuItem>Export</DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
    </ButtonGroup>
  )
}
```

## Input

Place an input between supporting buttons.

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

export function Example() {
  return (
    <ButtonGroup className="w-full max-w-sm">
      <input
        placeholder="Search…"
        className="h-9 min-w-0 flex-1 border border-border bg-transparent px-3 text-sm outline-none"
      />
      <Button variant="outline" size="icon" aria-label="Search">
        <SearchIcon />
      </Button>
    </ButtonGroup>
  )
}
```

## Input group

Keep the add control separate while joining the message field to its voice action.

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

export function Example() {
  return (
    <div className="flex w-full max-w-sm items-center gap-2">
      <Button variant="outline" size="icon" aria-label="Add">
        <PlusIcon />
      </Button>
      <ButtonGroup className="min-w-0 flex-1">
        <input
          placeholder="Send a message…"
          className="h-9 min-w-0 flex-1 border border-border bg-transparent px-3 text-sm outline-none"
        />
        <Button variant="outline" size="icon" aria-label="Voice message">
          <AudioLinesIcon />
        </Button>
      </ButtonGroup>
    </div>
  )
}
```

## Dropdown menu

Use `DropdownMenu` to create a split action.

```tsx
import {
  Button,
  ButtonGroup,
  buttonVariants,
} from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"

export function Example() {
  return (
    <ButtonGroup>
      <Button variant="outline">Follow</Button>
      <DropdownMenu>
        <DropdownMenuTrigger
          aria-label="Follow options"
          className={buttonVariants({ variant: "outline", size: "icon" })}
        >
          <ChevronDownIcon />
        </DropdownMenuTrigger>
        <DropdownMenuContent align="end">
          <DropdownMenuItem>Mute notifications</DropdownMenuItem>
          <DropdownMenuItem>Unfollow</DropdownMenuItem>
          <DropdownMenuItem>Block</DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
    </ButtonGroup>
  )
}
```

## Select

Pair a menu trigger with an input for a compact compound field.

```tsx
import { useState } from "react"
import {
  Button,
  ButtonGroup,
  buttonVariants,
} from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"

export function Example() {
  const [currency, setCurrency] = useState("$")

  return (
    <ButtonGroup>
      <DropdownMenu>
        <DropdownMenuTrigger
          className={buttonVariants({ variant: "outline" })}
        >
          {currency}
          <ChevronDownIcon data-icon="inline-end" />
        </DropdownMenuTrigger>
        <DropdownMenuContent align="start">
          <DropdownMenuItem onClick={() => setCurrency("$")}>$</DropdownMenuItem>
          <DropdownMenuItem onClick={() => setCurrency("€")}>€</DropdownMenuItem>
          <DropdownMenuItem onClick={() => setCurrency("£")}>£</DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
      <input
        placeholder="0.00"
        className="h-9 w-24 border border-border bg-transparent px-3 text-sm outline-none"
      />
      <Button variant="outline">Pay</Button>
    </ButtonGroup>
  )
}
```

## Popover

Open richer content from a dedicated chevron segment.

```tsx
import {
  Button,
  ButtonGroup,
  buttonVariants,
} from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"

export function Example() {
  return (
    <ButtonGroup>
      <Button variant="outline">
        <BotIcon data-icon="inline-start" />
        Copilot
      </Button>
      <DropdownMenu>
        <DropdownMenuTrigger
          aria-label="Open Copilot"
          className={buttonVariants({ variant: "outline", size: "icon" })}
        >
          <ChevronDownIcon />
        </DropdownMenuTrigger>
        <DropdownMenuContent align="end" className="w-64 p-3">
          <p className="m-0 mb-2 text-[13px] text-muted-foreground">
            Ask Copilot to draft, explain, or refactor.
          </p>
          <textarea
            rows={3}
            placeholder="Ask anything…"
            className="m-0 block w-full resize-none rounded-md border border-border bg-transparent px-2.5 py-2 text-sm outline-none placeholder:text-muted-foreground"
          />
        </DropdownMenuContent>
      </DropdownMenu>
    </ButtonGroup>
  )
}
```

## API Reference

### ButtonGroup

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

Nest multiple groups when related clusters need a little separation. See Nested above.

```tsx
<ButtonGroup>
  <Button variant="outline">Left</Button>
  <Button variant="outline">Right</Button>
</ButtonGroup>
```

### ButtonGroupSeparator

| Prop | Type | Default |
| --- | --- | --- |
| orientation | "horizontal" \| "vertical" | follows group |
| className | string | — |

```tsx
<ButtonGroup>
  <Button variant="outline">Save</Button>
  <ButtonGroupSeparator />
  <Button variant="outline">Cancel</Button>
</ButtonGroup>
```

### ButtonGroupText

| Prop | Type | Default |
| --- | --- | --- |
| asChild | boolean | false |
| className | string | — |

```tsx
<ButtonGroup>
  <ButtonGroupText>https://</ButtonGroupText>
  <Button variant="outline">Open</Button>
</ButtonGroup>
```

## Source

```tsx
"use client";

import {
  Children,
  cloneElement,
  createContext,
  isValidElement,
  useContext,
  type HTMLAttributes,
  type ReactElement,
  type ReactNode,
} from "react";
import { cn } from "@/lib/utils";

type ButtonGroupOrientation = "horizontal" | "vertical";
type ButtonGroupSlot = "single" | "first" | "middle" | "last";
type SeparatorOrientation = "horizontal" | "vertical";

type ButtonGroupContextValue = {
  orientation: ButtonGroupOrientation;
  slot: ButtonGroupSlot;
  overlap: boolean;
};

const ButtonGroupContext = createContext<ButtonGroupContextValue | null>(null);

export function useButtonGroup() {
  return useContext(ButtonGroupContext);
}

function slotFor(index: number, total: number): ButtonGroupSlot {
  if (total <= 1) return "single";
  if (index === 0) return "first";
  if (index === total - 1) return "last";
  return "middle";
}

function isSeparatorElement(child: ReactNode) {
  return isValidElement(child) && child.type === ButtonGroupSeparator;
}

type ButtonGroupProps = HTMLAttributes<HTMLDivElement> & {
  orientation?: ButtonGroupOrientation;
  children: ReactNode;
};

export function ButtonGroup({
  orientation = "horizontal",
  className,
  children,
  ...props
}: ButtonGroupProps) {
  const items = Children.toArray(children).filter(Boolean);
  const slottedCount = items.filter((child) => !isSeparatorElement(child))
    .length;
  let slottedIndex = 0;

  return (
    <div
      role="group"
      data-slot="button-group"
      data-orientation={orientation}
      className={cn(
        "inline-flex w-fit items-stretch",
        orientation === "horizontal" ? "flex-row" : "flex-col",
        "has-[>[data-slot=button-group]]:gap-2",
        "[&_[data-slot=dropdown-menu]]:inline-flex [&_[data-slot=dropdown-menu]]:self-stretch",
        "[&>input]:min-w-0 [&>input]:flex-1",
        className,
      )}
      {...props}
    >
      {items.map((child, index) => {
        const isSeparator = isSeparatorElement(child);
        const overlap =
          !isSeparator && index > 0 && !isSeparatorElement(items[index - 1]);
        const slot = isSeparator
          ? "middle"
          : slotFor(slottedIndex++, slottedCount);

        return (
          <ButtonGroupContext.Provider
            key={index}
            value={{ orientation, slot, overlap }}
          >
            {child}
          </ButtonGroupContext.Provider>
        );
      })}
    </div>
  );
}

type ButtonGroupSeparatorProps = HTMLAttributes<HTMLDivElement> & {
  orientation?: SeparatorOrientation;
};

export function ButtonGroupSeparator({
  orientation,
  className,
  ...props
}: ButtonGroupSeparatorProps) {
  const group = useButtonGroup();
  const resolved =
    orientation ??
    (group?.orientation === "vertical" ? "horizontal" : "vertical");

  return (
    <div
      role="separator"
      data-slot="button-group-separator"
      data-orientation={resolved}
      className={cn(
        "relative z-20 shrink-0 bg-background",
        resolved === "vertical" ? "w-px self-stretch" : "h-px w-full",
        className,
      )}
      {...props}
    />
  );
}
ButtonGroupSeparator.displayName = "ButtonGroupSeparator";

type ButtonGroupTextProps = HTMLAttributes<HTMLDivElement> & {
  asChild?: boolean;
};

export function ButtonGroupText({
  asChild = false,
  className,
  children,
  ...props
}: ButtonGroupTextProps) {
  const group = useButtonGroup();
  const classes = cn(
    "inline-flex items-center gap-2 border border-border bg-muted px-3 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
    buttonGroupItemClass(group),
    className,
  );

  if (asChild && isValidElement(children)) {
    const child = children as ReactElement<{ className?: string }>;
    return cloneElement(child, {
      className: cn(classes, child.props.className),
      ...props,
    });
  }

  return (
    <div data-slot="button-group-text" className={classes} {...props}>
      {children}
    </div>
  );
}

/** Rounding for the visible control face. */
export function buttonGroupFaceClass(
  group: ButtonGroupContextValue | null,
) {
  if (!group) return undefined;

  const { orientation, slot } = group;

  if (orientation === "horizontal") {
    return cn(
      slot === "single" && "!rounded-md",
      slot === "first" && "!rounded-l-md !rounded-r-none",
      slot === "middle" && "!rounded-none",
      slot === "last" && "!rounded-r-md !rounded-l-none",
    );
  }

  return cn(
    slot === "single" && "!rounded-md",
    slot === "first" && "!rounded-t-md !rounded-b-none",
    slot === "middle" && "!rounded-none",
    slot === "last" && "!rounded-b-md !rounded-t-none",
  );
}

/** Kill the double line at the join (inputs, outline, etc.). */
export function buttonGroupJoinClass(
  group: ButtonGroupContextValue | null,
) {
  if (!group?.overlap) return undefined;

  return group.orientation === "horizontal"
    ? "!border-l-transparent"
    : "!border-t-transparent";
}

/** Restore the join edge on hover for outlined controls only. */
export function buttonGroupOutlineJoinClass(
  group: ButtonGroupContextValue | null,
) {
  if (!group?.overlap) return undefined;

  return group.orientation === "horizontal"
    ? "hover:!border-l-border"
    : "hover:!border-t-border";
}

/** Overlap + stack for the flex item (button root or dropdown shell). */
export function buttonGroupOverlapClass(
  group: ButtonGroupContextValue | null,
) {
  if (!group || group.slot === "single") return undefined;

  return cn(
    "relative hover:z-10",
    group.overlap &&
      (group.orientation === "horizontal" ? "-ml-px" : "-mt-px"),
  );
}

export function buttonGroupItemClass(
  group: ButtonGroupContextValue | null,
) {
  return cn(
    buttonGroupFaceClass(group),
    buttonGroupOverlapClass(group),
    buttonGroupJoinClass(group),
  );
}
```
