> ## Documentation Index
> Fetch the complete documentation index at: https://docs.twenty.com/llms.txt
> Use this file to discover all available pages before exploring further.

# ButtonGroup

> Group related buttons with shared appearance and an optional frame.

export const StoryEmbed = ({storyId, title, height = 120}) => <>
    <Tabs>
      <Tab title="Light">
        <iframe title={`${title} (light)`} src={`https://storybook.twenty.com/iframe.html?id=${storyId}&viewMode=story&globals=colorScheme:light`} width="100%" height={height} loading="lazy" allow="clipboard-write" style={{
  border: 0
}} />
      </Tab>
      <Tab title="Dark">
        <iframe title={`${title} (dark)`} src={`https://storybook.twenty.com/iframe.html?id=${storyId}&viewMode=story&globals=colorScheme:dark`} width="100%" height={height} loading="lazy" allow="clipboard-write" style={{
  border: 0
}} />
      </Tab>
    </Tabs>
    <a href={`https://storybook.twenty.com/?path=/story/${storyId}`}>
      Open in Storybook
    </a>
  </>;

Give a group an accessible name and place related buttons inside it. The group joins adjacent borders and can apply defaults for size, variant, and color. Buttons retain their own tab stops.

<StoryEmbed storyId="ui-input-button-buttongroup--display" title="Button group" height={100} />

## Usage

```tsx theme={null}
import { IconCheckbox, IconNotes, IconTimelineEvent } from 'twenty-ui/icon';
import { Button, ButtonGroup } from 'twenty-ui/primitives/input';

export const ActivityActions = () => (
  <ButtonGroup aria-label="Create activity" size="sm">
    <Button startIcon={<IconNotes />}>Note</Button>
    <Button startIcon={<IconCheckbox />}>Task</Button>
    <Button startIcon={<IconTimelineEvent />}>Activity</Button>
  </ButtonGroup>
);
```

## State ownership

`ButtonGroup` coordinates appearance, not selection. It has no controlled or uncontrolled selection mode. Each button receives its own action handler and application state; see [Button's state ownership example](/ui/primitives/input/button#state-ownership) for loading behavior.

For a mutually exclusive choice, use [RadioGroup](/ui/primitives/input/radio-group), which owns selection for its radio controls.

## Appearance precedence

Each appearance prop resolves independently: the child's explicit prop, then the group default, then the component default. This applies to both Button and IconButton, including an explicit `IconButton size="xs"`. Defaults reach children through fragments, wrappers, and tooltip anchors.

Keep attached buttons at the same size so their edges align. This example overrides variant and color while inheriting the group size.

```tsx theme={null}
import { Button, ButtonGroup } from 'twenty-ui/primitives/input';

export const ActivityActions = () => (
  <ButtonGroup
    aria-label="Activity actions"
    variant="solid"
    color="accent"
    size="sm"
  >
    <Button variant="ghost" color="danger">
      Delete activity
    </Button>
    <Button color="neutral">Duplicate activity</Button>
    <Button>New activity</Button>
  </ButtonGroup>
);
```

<StoryEmbed storyId="ui-input-button-buttongroup--appearance-overrides" title="Individual appearance in a button group" height={100} />

## Detached controls

Set `attached={false}` to keep each control's rounded corners and a 2px gap. Group defaults still apply through wrappers and tooltip anchors; explicit child props take precedence.

## Framed controls

Set `framed` to add a translucent background, border, 2px padding, and concentric corners. The frame works with attached or detached controls and preserves explicit child appearance. Use `size="xs"` on [IconButton](/ui/components/input/icon-button) for compact 20px actions.

<StoryEmbed storyId="ui-components-iconbutton--framed-group-documentation" title="Framed button group" height={100} />

```tsx theme={null}
import { IconButton } from 'twenty-ui/components/input';
import { IconPlus, IconTrash } from 'twenty-ui/icon';
import { ButtonGroup } from 'twenty-ui/primitives/input';

export const CompactRecordActions = () => (
  <ButtonGroup framed attached={false} aria-label="Record actions">
    <IconButton size="xs" aria-label="Add" variant="ghost">
      <IconPlus />
    </IconButton>
    <IconButton size="xs" aria-label="Delete" variant="ghost" disabled>
      <IconTrash />
    </IconButton>
  </ButtonGroup>
);
```

## Updating existing groups

Replace `IconButtonGroup` and its `iconButtons` array with `ButtonGroup framed attached={false}` and explicit children. Replace `InsideButton` with `LightIconButton size="xs"`. Each button owns its accessible name, action handler, disabled state, and optional tooltip.

## Props

<ParamField body="attached" type="boolean" default="true">
  Join adjacent controls. Set to false for independently rounded buttons with a 2px gap.
</ParamField>

<ParamField body="color" type="&#x22;accent&#x22; | &#x22;danger&#x22; | &#x22;neutral&#x22; | &#x22;success&#x22;">
  Default semantic color for children without an explicit color.
</ParamField>

<ParamField body="framed" type="boolean" default="false">
  Add a translucent frame with padding and concentric corners. Button sizes and appearance remain independent unless set on the group.
</ParamField>

<ParamField body="render" type="ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, {}>">
  Allows you to replace the component's HTML element
  with a different tag, or compose it with another component.

  Accepts a `ReactElement` or a function that returns the element to render.
</ParamField>

<ParamField body="size" type="&#x22;md&#x22; | &#x22;sm&#x22;">
  Default button height for children without an explicit size.
</ParamField>

<ParamField body="variant" type="&#x22;ghost&#x22; | &#x22;link&#x22; | &#x22;outline&#x22; | &#x22;soft&#x22; | &#x22;solid&#x22;">
  Default surface treatment for children without an explicit variant.
</ParamField>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.