Westy Design Systemv0.2.7
OverviewComponentsExamplesStorybook

Foundations

  • Typography
  • Colors
  • Spacing
  • Radius
  • Shadows
  • Icons

Components

  • Accordion
  • Alert
  • Alert Dialog
  • Aspect Ratio
  • Avatar
  • Badge
  • Breadcrumb
  • Button
  • Button Group
  • Calendar
  • Card
  • Carousel
  • Chart
  • Checkbox
  • Collapsible
  • Combobox
  • Command
  • Context Menu
  • Data Table
  • Date Picker
  • Dialog
  • Direction
  • Drawer
  • Dropdown Menu
  • Empty
  • Field
  • Hover Card
  • Input
  • Input Group
  • Input OTP
  • Item
  • Kbd
  • Label
  • Menubar
  • Native Select
  • Navigation Menu
  • Pagination
  • Popover
  • Progress
  • Questionnaire
  • Radio Group
  • Resizable
  • Scroll Area
  • Select
  • Separator
  • Sheet
  • Sidebar
  • Skeleton
  • Slider
  • Sonner
  • Spinner
  • Switch
  • Table
  • Tabs
  • Textarea
  • Toggle
  • Toggle Group
  • Tooltip

Chat

  • Attachment
  • Bubble
  • Marker
  • Message
  • Message Scroller

Components

Button Group

A container that groups related buttons together with consistent styling.

Playground

—orientation
—variant
—size
—children
ButtonGroupSeparator
Usage
Wrap Buttons in a ButtonGroup. Inner corners square off, adjacent borders collapse, and the container renders role="group". Use it for buttons that perform actions — for buttons that toggle state, reach for Toggle Group instead.
import {
  ButtonGroup,
  ButtonGroupSeparator,
  ButtonGroupText,
} from "@workspace/ui/components/button-group"

<ButtonGroup aria-label="Text alignment">
  <Button variant="outline">Left</Button>
  <Button variant="outline">Center</Button>
  <Button variant="outline">Right</Button>
</ButtonGroup>
Orientation
The orientation prop stacks the group vertically — corners and borders collapse along the block axis instead.
<ButtonGroup orientation="vertical" aria-label="Zoom controls">
  <Button variant="outline" size="icon" aria-label="Zoom in">
    <PlusIcon />
  </Button>
  <Button variant="outline" size="icon" aria-label="Zoom out">
    <MinusIcon />
  </Button>
</ButtonGroup>
Size
The group has no size of its own — size the buttons inside it. Mixing a matching icon size at the end keeps the row square.
<ButtonGroup>
  <Button variant="outline" size="sm">Small</Button>
  <Button variant="outline" size="icon-sm" aria-label="Add">
    <PlusIcon />
  </Button>
</ButtonGroup>
<ButtonGroup>
  <Button variant="outline">Default</Button>
  <Button variant="outline" size="icon" aria-label="Add">
    <PlusIcon />
  </Button>
</ButtonGroup>
<ButtonGroup>
  <Button variant="outline" size="lg">Large</Button>
  <Button variant="outline" size="icon-lg" aria-label="Add">
    <PlusIcon />
  </Button>
</ButtonGroup>
Nested
Nest ButtonGroups to split one row into visually separate clusters — the outer group adds a gap-2 between its child groups while each inner group keeps its fused corners.
<ButtonGroup>
  <ButtonGroup>
    <Button variant="outline" size="icon" aria-label="Previous">
      <ArrowLeftIcon />
    </Button>
    <Button variant="outline" size="icon" aria-label="Next">
      <ArrowRightIcon />
    </Button>
  </ButtonGroup>
  <ButtonGroup>
    <Button variant="outline">Archive</Button>
    <Button variant="outline">Report</Button>
  </ButtonGroup>
</ButtonGroup>
Separator
Outline buttons separate themselves with their own borders. For filled variants, drop a ButtonGroupSeparator between buttons — a Separator tinted bg-input that stretches to the group's height.
<ButtonGroup>
  <Button variant="secondary" size="sm">Copy</Button>
  <ButtonGroupSeparator />
  <Button variant="secondary" size="sm">Paste</Button>
</ButtonGroup>
Split Button
The classic split button: a primary action plus a separated caret that opens a menu with the secondary ones.
<ButtonGroup>
  <Button variant="secondary">Update branch</Button>
  <ButtonGroupSeparator />
  <DropdownMenu>
    <DropdownMenuTrigger asChild>
      <Button variant="secondary" size="icon" aria-label="More options">
        <CaretDownIcon />
      </Button>
    </DropdownMenuTrigger>
    <DropdownMenuContent align="end">
      <DropdownMenuItem>Update with merge commit</DropdownMenuItem>
      <DropdownMenuItem>Update with rebase</DropdownMenuItem>
    </DropdownMenuContent>
  </DropdownMenu>
</ButtonGroup>
Text
ButtonGroupText renders static text that lines up with the buttons — a muted, bordered segment for prefixes and labels. Use asChild to render it as a Label tied to an input in the group.
<ButtonGroup>
  <ButtonGroupText asChild>
    <Label htmlFor="site-url">https://</Label>
  </ButtonGroupText>
  <Input id="site-url" placeholder="example.com" />
  <Button variant="outline">Visit</Button>
</ButtonGroup>
With Input
Inputs fuse into the group like buttons do and flex to fill the remaining space.
<ButtonGroup className="w-full max-w-sm">
  <Input placeholder="Search..." />
  <Button variant="outline" size="icon" aria-label="Search">
    <MagnifyingGlassIcon />
  </Button>
</ButtonGroup>
With Input Group
Nest an InputGroup for inputs with inline addons. Overriding --radius on the outer group is all it takes to make the whole row pill-shaped.
<ButtonGroup className="w-full max-w-sm [--radius:9999rem]">
  <ButtonGroup>
    <Button variant="outline" size="icon" aria-label="Attach">
      <PlusIcon />
    </Button>
  </ButtonGroup>
  <ButtonGroup>
    <InputGroup>
      <InputGroupInput placeholder="Send a message..." />
      <InputGroupAddon align="inline-end">
        <InputGroupButton size="icon-xs" aria-label="Voice mode">
          <MicrophoneIcon />
        </InputGroupButton>
      </InputGroupAddon>
    </InputGroup>
  </ButtonGroup>
</ButtonGroup>
With Select
Select triggers join the group too — the group trims the trigger to fit its content and keeps the fused corners.
<ButtonGroup>
  <ButtonGroup>
    <Select defaultValue="USD">
      <SelectTrigger className="font-mono">
        <SelectValue />
      </SelectTrigger>
      <SelectContent className="min-w-24">
        <SelectItem value="USD">USD</SelectItem>
        <SelectItem value="EUR">EUR</SelectItem>
        <SelectItem value="GBP">GBP</SelectItem>
      </SelectContent>
    </Select>
    <Input placeholder="10.00" pattern="[0-9]*" />
  </ButtonGroup>
  <ButtonGroup>
    <Button variant="outline" size="icon" aria-label="Send">
      <ArrowRightIcon />
    </Button>
  </ButtonGroup>
</ButtonGroup>
With Dropdown Menu
Pair a primary action with a caret trigger for the secondary ones — the outline borders act as the separator.
<ButtonGroup>
  <Button variant="outline">Follow</Button>
  <DropdownMenu>
    <DropdownMenuTrigger asChild>
      <Button variant="outline" size="icon" aria-label="More options">
        <CaretDownIcon />
      </Button>
    </DropdownMenuTrigger>
    <DropdownMenuContent align="end" className="w-52">
      <DropdownMenuGroup>
        <DropdownMenuItem>
          <SpeakerSlashIcon />
          Mute Conversation
        </DropdownMenuItem>
        <DropdownMenuItem>
          <CheckIcon />
          Mark as Read
        </DropdownMenuItem>
        <DropdownMenuItem>
          <WarningIcon />
          Report Conversation
        </DropdownMenuItem>
        <DropdownMenuItem>
          <UserMinusIcon />
          Block User
        </DropdownMenuItem>
      </DropdownMenuGroup>
      <DropdownMenuSeparator />
      <DropdownMenuItem variant="destructive">
        <TrashIcon />
        Delete Conversation
      </DropdownMenuItem>
    </DropdownMenuContent>
  </DropdownMenu>
</ButtonGroup>
With Popover
The same split pattern works with a Popover when the secondary action needs a richer surface than a menu.
<ButtonGroup>
  <Button variant="outline">
    <RobotIcon data-icon="inline-start" />
    Copilot
  </Button>
  <Popover>
    <PopoverTrigger asChild>
      <Button variant="outline" size="icon" aria-label="Open Popover">
        <CaretDownIcon />
      </Button>
    </PopoverTrigger>
    <PopoverContent align="end" className="text-sm">
      <PopoverHeader>
        <PopoverTitle>Start a new task with Copilot</PopoverTitle>
        <PopoverDescription>
          Describe your task in natural language.
        </PopoverDescription>
      </PopoverHeader>
      <Field>
        <FieldLabel htmlFor="task" className="sr-only">
          Task Description
        </FieldLabel>
        <Textarea id="task" placeholder="I need to..." className="resize-none" />
        <FieldDescription>
          Copilot will open a pull request for review.
        </FieldDescription>
      </Field>
    </PopoverContent>
  </Popover>
</ButtonGroup>
Accessibility
The container renders role="group" and Tab moves between the buttons inside it. Name the group with aria-label (or aria-labelledby) so screen readers announce what the actions have in common.
<ButtonGroup aria-label="Clipboard actions">
  <Button variant="outline">Copy</Button>
  <Button variant="outline">Paste</Button>
</ButtonGroup>
API Reference
ButtonGroup and ButtonGroupText render divs and accept every div attribute; ButtonGroupSeparator wraps Separator. Target data-slot="button-group" from a parent to restyle.
ComponentPropDefault
ButtonGrouporientation?: "horizontal" | "vertical""horizontal"
ButtonGroupSeparatororientation?: "horizontal" | "vertical""vertical"
ButtonGroupTextasChild?: booleanfalse
<ButtonGroup data-slot="button-group" role="group" />
<ButtonGroupSeparator data-slot="button-group-separator" />

Tokens used

5 design tokens consumed by the Button Group component.

  • --muted

    Muted

    bg-muted
  • --input

    Input

    bg-input
  • --border

    Border

    border
  • --radius-md

    Radius 0.8× (8px)

    rounded-mdrounded-e-md!rounded-b-md!
  • --spacing

    Spacing unit (4px)

    gap-2px-2.5