suiss/ui
  • Docs
  • Components
  • Patterns
  • Interactions
  • Charts
  • Blocks
  • AI
GitHubCreate
DocsComponentsPatternsInteractionsChartsBlocksAICreate
Components84
  • Overview
  • Accordion
  • Alert
  • Alert Dialog
  • Alert Sheet
  • Amount Input
  • Aspect Ratio
  • Attachment
  • Avatar
  • Badge
  • Bottom Bar
  • Breadcrumb
  • Bubble
  • Button
  • Button Group
  • Calendar
  • Card
  • Carousel
  • Chart
  • Checkbox
  • Chip
  • Code Block
  • Collapsible
  • Combobox
  • Command
  • Context Menu
  • Dialog
  • Direction
  • Drawer
  • Dropdown Menu
  • Empty
  • FAB
  • Field
  • Fullscreen Menu
  • Haptics
  • Header
  • Hover Card
  • Icon
  • Input
  • Input Accessory Bar
  • Input Group
  • Input OTP
  • Item
  • Kbd
  • Keyboard Scroll View
  • Keypad
  • Label
  • Marker
  • Menubar
  • Message
  • Message Scroller
  • Native Select
  • Navigation Menu
  • Page Indicator
  • Pagination
  • Pin Pad
  • Popover
  • Progress
  • QR Scanner
  • Questionnaire
  • Radio Group
  • Resizable
  • Screen
  • Scroll Area
  • Select
  • Separator
  • Sheet
  • Sidebar
  • Signature Pad
  • Skeleton
  • Slider
  • Spinner
  • Switch
  • Tab Bar
  • Table
  • Tabs
  • Text
  • Textarea
  • Toast
  • Toggle
  • Toggle Group
  • Tooltip
  • Trace
  • Wheel Picker
  • Widget

suiss/ui · Component

Button Group

A container that joins related buttons, inputs and menus into a single connected control.

Button Group is an open-source component from suiss/ui for React, React Native, SwiftUI and Jetpack Compose. The @esuiss/ui CLI copies its source into your project, so you own the code.

import {
  ArchiveIcon,
  ArrowLeftIcon,
  CalendarPlusIcon,
  ClockIcon,
  DotsThreeIcon,
  ListPlusIcon,
  TagIcon,
  TrashIcon,
} from "@phosphor-icons/react"
import { Button } from "@/components/ui/button"
import { ButtonGroup, ButtonGroupSeparator } from "@/components/ui/button-group"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,

A ButtonGroup lays out its children in a row or column and joins them visually: only the outer corners stay rounded, and borders between neighbors collapse. Use ButtonGroupSeparator to draw a divider between filled buttons, and ButtonGroupText to add a static label or prefix such as https://.

Groups can contain buttons, Input, InputGroup, Select triggers, and the triggers of dropdown menus and popovers. Nest ButtonGroup parts to create clusters with a gap between them. Use a button group for closely related actions, split buttons, or input-and-button combinations; for a set of toggleable options, use Toggle Group instead.

Installation

CLI

$ pnpm dlx shadcn@latest add @esuiss/button-group

Works in any project with a components.json. See Installation for the one-line registry setup. Or use the suiss/ui CLI:

$ pnpm dlx @esuiss/ui@latest add button-group

Manual

  1. Install the dependencies and add the cn helper:
$ pnpm add @base-ui/react class-variance-authority
  1. Add the pieces it builds on: Separator.

  2. Copy the source into components/ui/button-group.tsx:

components/ui/button-group.tsx
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { cva, type VariantProps } from "class-variance-authority"
import { Separator } from "@/components/ui/separator"
import { cn } from "@/lib/utils"

const buttonGroupVariants = cva(
  "has-[>[data-slot=button-group]]:gap-2 flex w-fit items-stretch *:focus-visible:relative *:focus-visible:z-10 [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 *:data-slot:bg-clip-border [&>[data-variant=ghost]]:rounded-full! [&>[data-variant=plain]]:rounded-full! has-[>[data-variant=outline]]:*:data-[slot=input-group]:border-border has-[>[data-variant=outline]]:*:data-[slot=select-trigger]:border-border has-[>[data-variant=outline]]:[&>input]:border-border",
  {
    variants: {
      orientation: {
        horizontal:
          "*:data-slot:rounded-e-none [&>[data-slot]:first-child]:rounded-s-full [&>[data-slot]:not(:has(~[data-slot]))]:rounded-e-full! [&>[data-slot]~[data-slot]]:rounded-s-none [&>[data-slot]~[data-slot]]:border-s-0",
        vertical:
          "flex-col *:data-slot:rounded-b-none [&>[data-slot]:first-child]:rounded-t-2xl [&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-2xl! [&>[data-slot]~[data-slot]]:rounded-t-none [&>[data-slot]~[data-slot]]:border-t-0",
      },
  1. Update the import paths to match your project.

Usage

import { ButtonGroup, ButtonGroupText, ButtonGroupSeparator } from "@/components/ui/button-group"
<ButtonGroup orientation="horizontal">
  <ButtonGroup>
    <Button variant="secondary" size="icon" aria-label="Go back">
      <ArrowLeftIcon />
    </Button>
  </ButtonGroup>
  <ButtonGroup>
    <Button variant="secondary">Archive</Button>
    <ButtonGroupSeparator />
    <Button variant="secondary">Report</Button>
  </ButtonGroup>
  <ButtonGroup>
    <Button variant="secondary">Snooze</Button>
    <ButtonGroupSeparator />
    <DropdownMenu>
      <DropdownMenuTrigger
        render={
          <Button
            variant="secondary"
            size="icon"
            aria-label="More options"
          />
        }
      >
        <DotsThreeIcon />
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end" className="w-52">
        <DropdownMenuGroup>
          <DropdownMenuItem>
            <ListPlusIcon />
            Mark as read
          </DropdownMenuItem>
          <DropdownMenuItem>
            <ArchiveIcon />
            Archive
          </DropdownMenuItem>
        </DropdownMenuGroup>
        <DropdownMenuSeparator />
        <DropdownMenuGroup>
          <DropdownMenuItem>
            <ClockIcon />
            Snooze
          </DropdownMenuItem>
          <DropdownMenuItem>
            <CalendarPlusIcon />
            Add to calendar
          </DropdownMenuItem>
          <DropdownMenuItem>
            <TagIcon />
            Label as
          </DropdownMenuItem>
        </DropdownMenuGroup>
        <DropdownMenuSeparator />
        <DropdownMenuGroup>
          <DropdownMenuItem variant="destructive">
            <TrashIcon />
            Trash
          </DropdownMenuItem>
        </DropdownMenuGroup>
      </DropdownMenuContent>
    </DropdownMenu>
  </ButtonGroup>
</ButtonGroup>
<ButtonGroup orientation="horizontal">
  <ButtonGroup>
    <Button variant="secondary" size="icon" aria-label="Go back">
      <ArrowLeftIcon />
    </Button>
  </ButtonGroup>
  <ButtonGroup>
    <Button variant="secondary">Archive</Button>
    <ButtonGroupSeparator />
    <Button variant="secondary">Report</Button>
  </ButtonGroup>
  <ButtonGroup>
    <Button variant="secondary">Snooze</Button>
    <ButtonGroupSeparator />
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="secondary" size="icon" aria-label="More options">
          <DotsThreeIcon />
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end" className="w-52">
        <DropdownMenuGroup>
          <DropdownMenuItem>
            <ListPlusIcon />
            Mark as read
          </DropdownMenuItem>
          <DropdownMenuItem>
            <ArchiveIcon />
            Archive
          </DropdownMenuItem>
        </DropdownMenuGroup>
        <DropdownMenuSeparator />
        <DropdownMenuGroup>
          <DropdownMenuItem>
            <ClockIcon />
            Snooze
          </DropdownMenuItem>
          <DropdownMenuItem>
            <CalendarPlusIcon />
            Add to calendar
          </DropdownMenuItem>
          <DropdownMenuItem>
            <TagIcon />
            Label as
          </DropdownMenuItem>
        </DropdownMenuGroup>
        <DropdownMenuSeparator />
        <DropdownMenuGroup>
          <DropdownMenuItem variant="destructive">
            <TrashIcon />
            Trash
          </DropdownMenuItem>
        </DropdownMenuGroup>
      </DropdownMenuContent>
    </DropdownMenu>
  </ButtonGroup>
</ButtonGroup>
<ButtonGroup orientation="horizontal">
  <ButtonGroup>
    <Button variant="secondary" size="icon" aria-label="Go back">
      <ArrowLeftIcon />
    </Button>
  </ButtonGroup>
  <ButtonGroup>
    <Button variant="secondary">Archive</Button>
    <ButtonGroupSeparator />
    <Button variant="secondary">Report</Button>
  </ButtonGroup>
  <ButtonGroup>
    <Button variant="secondary">Snooze</Button>
    <ButtonGroupSeparator />
    <DropdownMenu>
      <Button variant="secondary" size="icon" aria-label="More options">
        <DotsThreeIcon />
      </Button>
      <DropdownMenuContent placement="bottom end" className="w-52">
        <DropdownMenuGroup>
          <DropdownMenuItem>
            <ListPlusIcon />
            Mark as read
          </DropdownMenuItem>
          <DropdownMenuItem>
            <ArchiveIcon />
            Archive
          </DropdownMenuItem>
        </DropdownMenuGroup>
        <DropdownMenuSeparator />
        <DropdownMenuGroup>
          <DropdownMenuItem>
            <ClockIcon />
            Snooze
          </DropdownMenuItem>
          <DropdownMenuItem>
            <CalendarPlusIcon />
            Add to calendar
          </DropdownMenuItem>
          <DropdownMenuItem>
            <TagIcon />
            Label as
          </DropdownMenuItem>
        </DropdownMenuGroup>
        <DropdownMenuSeparator />
        <DropdownMenuGroup>
          <DropdownMenuItem variant="destructive">
            <TrashIcon />
            Trash
          </DropdownMenuItem>
        </DropdownMenuGroup>
      </DropdownMenuContent>
    </DropdownMenu>
  </ButtonGroup>
</ButtonGroup>

Elements are joined only when they expose a data-slot attribute, as the library's components do.

When a group contains other ButtonGroup parts, the outer group adds a gap between them instead of joining them.

ButtonGroupSeparator is vertical by default. In a vertical group, pass orientation="horizontal". Separators are most useful between secondary or default buttons; outline buttons already show their borders.

Give the group an aria-label when its purpose is not clear from its contents, as in the pagination and media control examples.

Examples

Outline

The same toolbar built with outline buttons and no separators.

import {
  ArchiveIcon,
  ArrowLeftIcon,
  CalendarPlusIcon,
  ClockIcon,
  DotsThreeIcon,
  ListPlusIcon,
  TagIcon,
  TrashIcon,
} from "@phosphor-icons/react"
import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,

Orientation

Stacks zoom buttons vertically with orientation="vertical".

import { MinusIcon, PlusIcon } from "@phosphor-icons/react"
import { Button } from "@/components/ui/button"
import { ButtonGroup, ButtonGroupSeparator } from "@/components/ui/button-group"

export default function ButtonGroupOrientation() {
  return (
    <ButtonGroup orientation="vertical" aria-label="Media controls">
      <Button variant="secondary" size="icon" aria-label="Zoom in">
        <PlusIcon />
      </Button>
      <ButtonGroupSeparator orientation="horizontal" />
      <Button variant="secondary" size="icon" aria-label="Zoom out">
        <MinusIcon />
      </Button>
    </ButtonGroup>
  )

Sizes

Groups of sm, default and lg buttons.

import { PlusIcon } from "@phosphor-icons/react"
import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"

export default function ButtonGroupSizes() {
  return (
    <div className="flex flex-col items-start gap-6">
      <ButtonGroup orientation="horizontal">
        <Button variant="outline" size="sm">
          Small
        </Button>
        <Button variant="outline" size="sm">
          Button
        </Button>
        <Button variant="outline" size="sm">
          Group

Nested

Nests groups to separate page numbers from previous and next buttons.

import { ArrowLeftIcon, ArrowRightIcon } from "@phosphor-icons/react"
import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"

export default function ButtonGroupNested() {
  return (
    <ButtonGroup orientation="horizontal">
      <ButtonGroup>
        <Button variant="outline" size="sm">
          1
        </Button>
        <Button variant="outline" size="sm">
          2
        </Button>
        <Button variant="outline" size="sm">
          3

With separator

Divides two secondary buttons with ButtonGroupSeparator.

import { CopyIcon, ShareIcon } from "@phosphor-icons/react"
import { Button } from "@/components/ui/button"
import { ButtonGroup, ButtonGroupSeparator } from "@/components/ui/button-group"

export default function ButtonGroupWithSeparator() {
  return (
    <ButtonGroup orientation="horizontal">
      <Button variant="secondary">
        <CopyIcon data-icon="inline-start" />
        Copy
      </Button>
      <ButtonGroupSeparator />
      <Button variant="secondary">
        <ShareIcon data-icon="inline-start" />
        Share
      </Button>

Plain

Groups plain buttons with leading and trailing icons.

import { ArrowLeftIcon, ArrowRightIcon, ShareIcon } from "@phosphor-icons/react"
import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"

export default function ButtonGroupPlain() {
  return (
    <ButtonGroup orientation="horizontal">
      <Button variant="plain">
        <ArrowLeftIcon data-icon="inline-start" />
        Back
      </Button>
      <Button variant="plain">
        Forward
        <ArrowRightIcon data-icon="inline-end" />
      </Button>
      <Button variant="plain">

Split button

Pairs a primary action with an icon button for more options.

import { CaretDownIcon } from "@phosphor-icons/react"
import { Button } from "@/components/ui/button"
import { ButtonGroup, ButtonGroupSeparator } from "@/components/ui/button-group"

export default function ButtonGroupSplitButton() {
  return (
    <ButtonGroup orientation="horizontal">
      <Button>Publish</Button>
      <ButtonGroupSeparator className="bg-primary-foreground/30" />
      <Button size="icon" aria-label="More options">
        <CaretDownIcon />
      </Button>
    </ButtonGroup>
  )
}

With input

Attaches a search button to an Input.

import { MagnifyingGlassIcon } from "@phosphor-icons/react"
import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"
import { Input } from "@/components/ui/input"

export default function ButtonGroupWithInput() {
  return (
    <ButtonGroup orientation="horizontal">
      <Input aria-label="Search" placeholder="Search..." />
      <Button variant="secondary" size="icon-lg" aria-label="Search">
        <MagnifyingGlassIcon />
      </Button>
    </ButtonGroup>
  )
}

With input group

Combines an add button with an InputGroup that has an inline voice button.

import { MicrophoneIcon, PlusIcon } from "@phosphor-icons/react"
import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"
import {
  InputGroup,
  InputGroupAddon,
  InputGroupButton,
  InputGroupInput,
} from "@/components/ui/input-group"
import {
  Tooltip,
  TooltipContent,
  TooltipTrigger,
} from "@/components/ui/tooltip"

export default function ButtonGroupWithInputGroup() {

With text

Prefixes an input with a ButtonGroupText label and ends with a save button.

import { LinkIcon } from "@phosphor-icons/react"
import { Button } from "@/components/ui/button"
import { ButtonGroup, ButtonGroupText } from "@/components/ui/button-group"
import { Input } from "@/components/ui/input"

export default function ButtonGroupWithText() {
  return (
    <ButtonGroup orientation="horizontal">
      <ButtonGroupText>
        <LinkIcon />
        https://
      </ButtonGroupText>
      <Input aria-label="Website" placeholder="example.com" />
      <Button variant="secondary" size="lg">
        Save
      </Button>

With dropdown menu

Opens a dropdown menu from the second half of a split button.

import {
  CaretDownIcon,
  CopyIcon,
  ShareIcon,
  UserCircleMinusIcon,
  WarningCircleIcon,
} from "@phosphor-icons/react"
import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,

With select

Joins a currency Select to an amount input.

import { ArrowRightIcon } from "@phosphor-icons/react"
import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"
import { Input } from "@/components/ui/input"
import {
  Select,
  SelectContent,
  SelectGroup,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"

const currencies = [
  { value: "usd", label: "$" },
  { value: "eur", label: "€" },

With popover

Opens a popover from the second half of a split button.

import { CaretDownIcon, SparkleIcon } from "@phosphor-icons/react"
import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"
import {
  Popover,
  PopoverContent,
  PopoverDescription,
  PopoverHeader,
  PopoverTitle,
  PopoverTrigger,
} from "@/components/ui/popover"
import { Textarea } from "@/components/ui/textarea"

export default function ButtonGroupWithPopover() {
  return (
    <ButtonGroup orientation="horizontal">

Pagination

A labelled group of page buttons that marks the current page with aria-current.

import { CaretLeftIcon, CaretRightIcon } from "@phosphor-icons/react"
import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"

export default function ButtonGroupPagination() {
  return (
    <ButtonGroup orientation="horizontal" aria-label="Pagination">
      <Button variant="outline" size="icon" aria-label="Previous page">
        <CaretLeftIcon />
      </Button>
      <Button variant="outline">1</Button>
      <Button variant="outline" aria-current="page">
        2
      </Button>
      <Button variant="outline">3</Button>
      <Button variant="outline" size="icon" aria-label="Next page">

API Reference

Anatomy3 parts
<ButtonGroup>
<ButtonGroupSeparator />
<ButtonGroupText />
</ButtonGroup>
01

ButtonGroup

The container that joins its children and sets the orientation.

PropTypeDefaultDescription
orientation"horizontal" | "vertical""horizontal"Direction of the group. horizontal joins items in a row with pill ends; vertical stacks them with rounded top and bottom.

Also accepts the props of React.ComponentProps<"div">.

02

ButtonGroupText

A static, button-shaped label for text or icons.

Also accepts the props of useRender.ComponentProps<"div">.

03

ButtonGroupSeparator

A thin divider between adjacent items.

Also accepts the props of React.ComponentProps<typeof Separator>.

Accessibility

  • ButtonGroup renders with role="group"; add an aria-label to name it.
  • Each button, input and trigger remains a separate tab stop in document order.
  • Icon-only buttons inside a group need an aria-label.
  • ButtonGroupText renders a div and is not focusable; use render to render a label element if it labels an input.

Notes

  • Styling hooks: data-slot="button-group", "button-group-text" and "button-group-separator", plus data-orientation on the group.
  • Focused items are raised with z-10 so their focus ring is not clipped by neighbors.
  • In groups containing outline buttons, inputs, input groups and select triggers also get the border-border color.
iOS
Button Group on iOS in light modeButton Group on iOS in dark mode
import { ArchiveIcon, ArrowLeftIcon, CalendarPlusIcon, ClockIcon, DotsThreeIcon, ListPlusIcon, TagIcon, TrashIcon } from "phosphor-react-native"
import { Button } from "@/components/ui/button"
import { ButtonGroup, ButtonGroupSeparator } from "@/components/ui/button-group"
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Icon } from "@/components/ui/icon"
import { Text } from "@/components/ui/text"

function MailMenuContent() {
  return (
    <DropdownMenuContent align="end" className="w-52">
      <DropdownMenuGroup>
        <DropdownMenuItem>
          <Icon as={ListPlusIcon} className="size-4" />
          <Text>Mark as read</Text>
        </DropdownMenuItem>
        <DropdownMenuItem>
import EsuissUI
import PhosphorSwift
import SwiftUI

struct ButtonGroupExample: View {
  var body: some View {
    ButtonGroup {
      ButtonGroup {
        ButtonGroupIconButton(icon: Ph.arrowLeft.regular, label: "Go back", variant: .secondary)
      }
      ButtonGroup {
        Button("Archive") {}.buttonStyle(.variant(.secondary))
        ButtonGroupSeparator()
        Button("Report") {}.buttonStyle(.variant(.secondary))
      }
      ButtonGroup {
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.semantics.contentDescription
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.Archive
import com.adamglin.phosphoricons.regular.ArrowLeft
import com.adamglin.phosphoricons.regular.CalendarPlus
import com.adamglin.phosphoricons.regular.Clock
import com.adamglin.phosphoricons.regular.DotsThree
import com.adamglin.phosphoricons.regular.ListPlus
import com.adamglin.phosphoricons.regular.Tag
import com.adamglin.phosphoricons.regular.Trash

ButtonGroup is a View with the group role that joins its children: only the outer ends keep their rounded corners and the shared borders collapse. It works with Button and with other controls such as inputs, selects and menu triggers. ghost and plain buttons are left unjoined.

Nest groups to keep separate clusters in one row with a gap between them. ButtonGroupSeparator draws a divider between joined buttons, and ButtonGroupText shows a label or icon in the same pill shape.

Installation

$ pnpm dlx shadcn@latest add @esuiss-rn/button-group

See Installation for the components.json setup. Or use the suiss/ui CLI:

$ pnpm dlx @esuiss/ui@latest add button-group
$ pnpm dlx @esuiss/ui@latest add button-group --platform swift
$ pnpm dlx @esuiss/ui@latest add button-group --platform kotlin

The CLI also adds what it builds on: Button, Icon, Separator, Text.

Manual

  1. Install the dependencies:
npx expo install class-variance-authority cn
  1. Add the pieces it builds on: Button, Icon, Separator, Text, and the primitives components/primitives/types.tsx, components/primitives/use-render.tsx.

  2. Copy the source into components/ui/button-group.tsx:

uniwind/components/ui/button-group.tsx
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"
import * as React from "react"
import { View, type ViewProps } from "react-native"
import type { ClassNameProp, RenderProp } from "@/components/primitives/types"
import { resolveClassName, useRender } from "@/components/primitives/use-render"
import {
  Button,
  ButtonClassContext,
  type ButtonVariant,
} from "@/components/ui/button"
import { IconClassContext } from "@/components/ui/icon"
import { Separator } from "@/components/ui/separator"
import { TextClassContext } from "@/components/ui/text"

type ButtonGroupOrientation = "horizontal" | "vertical"
  1. Update the import paths to match your project.
  1. Install the dependencies:
npx expo install class-variance-authority cn
  1. Add the pieces it builds on: Button, Icon, Separator, Text, and the primitives components/primitives/types.tsx, components/primitives/use-render.tsx.

  2. Copy the source into components/ui/button-group.tsx:

nativewind/components/ui/button-group.tsx
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"
import * as React from "react"
import { View, type ViewProps } from "react-native"
import type { ClassNameProp, RenderProp } from "@/components/primitives/types"
import { resolveClassName, useRender } from "@/components/primitives/use-render"
import {
  Button,
  ButtonClassContext,
  type ButtonVariant,
} from "@/components/ui/button"
import { IconClassContext } from "@/components/ui/icon"
import { Separator } from "@/components/ui/separator"
import { TextClassContext } from "@/components/ui/text"

type ButtonGroupOrientation = "horizontal" | "vertical"
  1. Update the import paths to match your project.
  1. Add the pieces it builds on: Button, Icon, Separator, Text, and the primitives components/primitives/types.tsx, components/primitives/use-render.tsx.

  2. Copy the source into components/ui/button-group.tsx:

stylesheet/components/ui/button-group.tsx
import * as React from "react"
import {
  type StyleProp,
  StyleSheet,
  View,
  type ViewProps,
  type ViewStyle,
} from "react-native"
import type { RenderProp } from "@/components/primitives/types"
import { resolveStyle, useRender } from "@/components/primitives/use-render"
import {
  Button,
  ButtonStyleContext,
  type ButtonVariant,
} from "@/components/ui/button"
import { IconSizeContext } from "@/components/ui/icon"
  1. Update the import paths to match your project.
  1. Target iOS 17 or later.

  2. Copy the theme files every view uses:

swift/Theme/Theme.swift
import CoreText
import SwiftUI
import UIKit

struct RGBA: Sendable {
  let red: CGFloat
  let green: CGFloat
  let blue: CGFloat
  let alpha: CGFloat

  init(_ hex: UInt32, _ alpha: CGFloat = 1) {
    red = CGFloat((hex >> 16) & 0xFF) / 255
    green = CGFloat((hex >> 8) & 0xFF) / 255
    blue = CGFloat(hex & 0xFF) / 255
    self.alpha = alpha
  }
swift/Theme/Tokens.swift
import SwiftUI

public enum EsuissColor {
  public static let background = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x000000))
  public static let foreground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let card = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x1D1D1F))
  public static let cardForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let popover = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x1D1D1F))
  public static let popoverForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let primary = Color.adaptive(light: .init(0x0071E3), dark: .init(0x2997FF))
  public static let primaryForeground = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0xFFFFFF))
  public static let secondary = Color.adaptive(light: .init(0x000000, 0.04), dark: .init(0xFFFFFF, 0.1))
  public static let secondaryForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let muted = Color.adaptive(light: .init(0xF5F5F7), dark: .init(0x161617))
  public static let mutedForeground = Color.adaptive(light: .init(0x6E6E73), dark: .init(0xA1A1A6))
  public static let accent = Color.adaptive(light: .init(0x000000, 0.06), dark: .init(0xFFFFFF, 0.1))
  1. Copy the source and every file it uses:
swift/Components/ButtonGroup.swift
import SwiftUI

extension EnvironmentValues {
  @Entry var buttonGroup: Axis? = nil
  @Entry var buttonGroupSize: CGSize = .zero
}

enum ButtonGroupSpace: Hashable {
  case group
}

struct ButtonGroupFrame {
  let bounds: Anchor<CGRect>
  let outlined: Bool
}
swift/Components/Button.swift
import SwiftUI

public struct VariantButtonStyle: ButtonStyle {
  public enum Variant: CaseIterable, Sendable {
    case `default`, outline, secondary, tinted, ghost, plain, destructive, link
  }

  public enum Size: CaseIterable, Sendable {
    case `default`, xs, sm, lg, xl, icon, iconXs, iconSm, iconLg, iconXl

    var height: CGFloat {
      switch self {
      case .xs, .iconXs: Metrics.Height.buttonXs
      case .sm, .iconSm: Metrics.Height.buttonSm
      case .default, .icon: Metrics.Height.button
      case .lg, .iconLg: Metrics.Height.buttonLg
swift/Components/TextVariant.swift
import SwiftUI

public enum TextVariant: CaseIterable, Sendable {
  case `default`, h1, h2, h3, h4, p, blockquote, code, lead, large, small, muted

  var style: TextStyle {
    switch self {
    case .h1: .xl3
    case .h2: .xl2
    case .h3: .xl
    case .h4, .lead, .large: .lg
    case .code, .small, .muted: .sm
    default: .base
    }
  }
  1. Add the files to your app target.
  1. Set minSdk to 24 or higher and add the dependencies:
build.gradle.kts
implementation(platform("androidx.compose:compose-bom:2026.09.00"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.ui:ui")
  1. Copy the theme files every view uses:
kotlin/theme/Theme.kt
package com.esuiss.ui.theme

import android.content.res.AssetManager
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.dropShadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.shadow.Shadow as DropShadow
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
kotlin/theme/Tokens.kt
package com.esuiss.ui.theme

import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp

@Immutable
data class Colors(
    val background: Color,
    val foreground: Color,
    val card: Color,
    val cardForeground: Color,
    val popover: Color,
    val popoverForeground: Color,
    val primary: Color,
  1. Copy the source and every file it uses:
kotlin/components/ButtonGroup.kt
package com.esuiss.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Immutable
kotlin/components/Content.kt
package com.esuiss.ui.components

import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

val LocalContentStyle = compositionLocalOf { TextStyle.Default }

val LocalContentColor = compositionLocalOf { Color.Unspecified }

val LocalIconSize = compositionLocalOf { 20.dp }
  1. Update the com.esuiss.ui package names to match your project.

Usage

import { ButtonGroup, ButtonGroupText, ButtonGroupSeparator } from "@/components/ui/button-group"
<ButtonGroup>
  <ButtonGroup>
    <Button variant="secondary" size="icon" accessibilityLabel="Go back">
      <Icon as={ArrowLeftIcon} />
    </Button>
  </ButtonGroup>
  <ButtonGroup>
    <Button variant="secondary">
      <Text>Archive</Text>
    </Button>
    <ButtonGroupSeparator />
    <Button variant="secondary">
      <Text>Report</Text>
    </Button>
  </ButtonGroup>
  <ButtonGroup>
    <Button variant="secondary">
      <Text>Snooze</Text>
    </Button>
    <ButtonGroupSeparator />
    <DropdownMenu>
      <DropdownMenuTrigger
        accessibilityLabel="More options"
        render={<Button variant="secondary" size="icon" />}
      >
        <Icon as={DotsThreeIcon} />
      </DropdownMenuTrigger>
      <MailMenuContent />
    </DropdownMenu>
  </ButtonGroup>
</ButtonGroup>

Set orientation="vertical" to stack the buttons. Use a separator between buttons of the same filled variant, such as in a split button, where there is no border to divide them.

Give icon-only buttons an accessibilityLabel, and give the group one when its purpose is not clear from its contents.

Examples

Outline

Nested groups of outline buttons.

iOS
Button Group (outline) on iOS in light modeButton Group (outline) on iOS in dark mode
import { ArchiveIcon, ArrowLeftIcon, CalendarPlusIcon, ClockIcon, DotsThreeIcon, ListPlusIcon, TagIcon, TrashIcon } from "phosphor-react-native"
import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Icon } from "@/components/ui/icon"
import { Text } from "@/components/ui/text"

function MailMenuContent() {
  return (
    <DropdownMenuContent align="end" className="w-52">
      <DropdownMenuGroup>
        <DropdownMenuItem>
          <Icon as={ListPlusIcon} className="size-4" />
          <Text>Mark as read</Text>
        </DropdownMenuItem>
        <DropdownMenuItem>
import EsuissUI
import PhosphorSwift
import SwiftUI

struct ButtonGroupOutlineExample: View {
  var body: some View {
    ButtonGroup {
      ButtonGroup {
        ButtonGroupIconButton(icon: Ph.arrowLeft.regular, label: "Go back", variant: .outline)
      }
      ButtonGroup {
        Button("Archive") {}.buttonStyle(.variant(.outline))
        Button("Report") {}.buttonStyle(.variant(.outline))
      }
      ButtonGroup {
        Button("Snooze") {}.buttonStyle(.variant(.outline))
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.semantics.contentDescription
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.Archive
import com.adamglin.phosphoricons.regular.ArrowLeft
import com.adamglin.phosphoricons.regular.CalendarPlus
import com.adamglin.phosphoricons.regular.Clock
import com.adamglin.phosphoricons.regular.DotsThree
import com.adamglin.phosphoricons.regular.ListPlus
import com.adamglin.phosphoricons.regular.Tag
import com.adamglin.phosphoricons.regular.Trash

Orientation

A vertical group of zoom buttons with a horizontal separator.

iOS
Button Group (orientation) on iOS in light modeButton Group (orientation) on iOS in dark mode
import { MinusIcon, PlusIcon } from "phosphor-react-native"
import { Button } from "@/components/ui/button"
import { ButtonGroup, ButtonGroupSeparator } from "@/components/ui/button-group"
import { Icon } from "@/components/ui/icon"

export function ButtonGroupOrientationExample() {
  return (
    <ButtonGroup orientation="vertical" accessibilityLabel="Media controls">
      <Button variant="secondary" size="icon" accessibilityLabel="Zoom in">
        <Icon as={PlusIcon} />
      </Button>
      <ButtonGroupSeparator orientation="horizontal" />
      <Button variant="secondary" size="icon" accessibilityLabel="Zoom out">
        <Icon as={MinusIcon} />
      </Button>
    </ButtonGroup>
import EsuissUI
import PhosphorSwift
import SwiftUI

struct ButtonGroupOrientationExample: View {
  var body: some View {
    ButtonGroup(orientation: .vertical) {
      ButtonGroupIconButton(icon: Ph.plus.regular, label: "Zoom in", variant: .secondary)
      ButtonGroupSeparator(.horizontal)
      ButtonGroupIconButton(icon: Ph.minus.regular, label: "Zoom out", variant: .secondary)
    }
    .accessibilityLabel(Text("Media controls"))
  }
}

struct ButtonGroupIcon: View {
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.Minus
import com.adamglin.phosphoricons.regular.Plus
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonGroup
import com.esuiss.ui.components.ButtonGroupOrientation
import com.esuiss.ui.components.ButtonGroupSeparator
import com.esuiss.ui.components.ButtonSize
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Icon

Sizes

Groups at different button sizes.

iOS
Button Group (sizes) on iOS in light modeButton Group (sizes) on iOS in dark mode
import { PlusIcon } from "phosphor-react-native"
import { View } from "react-native"
import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"
import { Icon } from "@/components/ui/icon"
import { Text } from "@/components/ui/text"

export function ButtonGroupSizesExample() {
  return (
    <View className="items-start gap-6">
      <ButtonGroup>
        <Button variant="outline" size="sm">
          <Text>Small</Text>
        </Button>
        <Button variant="outline" size="sm">
          <Text>Button</Text>
import EsuissUI
import PhosphorSwift
import SwiftUI

struct ButtonGroupSizesExample: View {
  var body: some View {
    VStack(alignment: .leading, spacing: 24) {
      ButtonGroupSizeRow(title: "Small", size: .sm, iconSize: .iconSm)
      ButtonGroupSizeRow(title: "Default", size: .default, iconSize: .icon)
      ButtonGroupSizeRow(title: "Large", size: .lg, iconSize: .iconLg)
    }
  }
}

struct ButtonGroupIcon: View {
  let icon: Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.Plus
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonGroup
import com.esuiss.ui.components.ButtonSize
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Icon

@Composable
fun ButtonGroupIconButton(

Nested

Groups inside a group, spaced apart.

iOS
Button Group (nested) on iOS in light modeButton Group (nested) on iOS in dark mode
import { ArrowLeftIcon, ArrowRightIcon } from "phosphor-react-native"
import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"
import { Icon } from "@/components/ui/icon"
import { Text } from "@/components/ui/text"

export function ButtonGroupNestedExample() {
  return (
    <ButtonGroup>
      <ButtonGroup>
        {["1", "2", "3", "4", "5"].map((page) => (
          <Button key={page} variant="outline" size="sm">
            <Text>{page}</Text>
          </Button>
        ))}
      </ButtonGroup>
import EsuissUI
import PhosphorSwift
import SwiftUI

struct ButtonGroupNestedExample: View {
  var body: some View {
    ButtonGroup {
      ButtonGroup {
        ForEach(["1", "2", "3", "4", "5"], id: \.self) { page in
          Button {} label: { Text(verbatim: page) }
            .buttonStyle(.variant(.outline, size: .sm))
        }
      }
      ButtonGroup {
        ButtonGroupIconButton(icon: Ph.arrowLeft.regular, label: "Previous", variant: .outline, size: .iconSm)
        ButtonGroupIconButton(icon: Ph.arrowRight.regular, label: "Next", variant: .outline, size: .iconSm)
import androidx.compose.runtime.Composable
import androidx.compose.ui.semantics.contentDescription
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.ArrowLeft
import com.adamglin.phosphoricons.regular.ArrowRight
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonGroup
import com.esuiss.ui.components.ButtonSize
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Icon

@Composable
fun ButtonGroupIconButton(
    label: String,
    icon: androidx.compose.ui.graphics.vector.ImageVector,

With separator

Buttons divided by ButtonGroupSeparator.

iOS
Button Group (with separator) on iOS in light modeButton Group (with separator) on iOS in dark mode
import { CopyIcon, ShareIcon } from "phosphor-react-native"
import { Button } from "@/components/ui/button"
import { ButtonGroup, ButtonGroupSeparator } from "@/components/ui/button-group"
import { Icon } from "@/components/ui/icon"
import { Text } from "@/components/ui/text"

export function ButtonGroupWithSeparatorExample() {
  return (
    <ButtonGroup>
      <Button variant="secondary">
        <Icon as={CopyIcon} />
        <Text>Copy</Text>
      </Button>
      <ButtonGroupSeparator />
      <Button variant="secondary">
        <Icon as={ShareIcon} />
import EsuissUI
import PhosphorSwift
import SwiftUI

struct ButtonGroupWithSeparatorExample: View {
  var body: some View {
    ButtonGroup {
      Button {} label: {
        Label { Text("Copy") } icon: { Ph.copy.regular }
      }
      .buttonStyle(.variant(.secondary))
      ButtonGroupSeparator()
      Button {} label: {
        Label { Text("Share") } icon: { Ph.share.regular }
      }
      .buttonStyle(.variant(.secondary))
import androidx.compose.runtime.Composable
import androidx.compose.ui.semantics.contentDescription
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.Copy
import com.adamglin.phosphoricons.regular.Share
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonGroup
import com.esuiss.ui.components.ButtonGroupSeparator
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Text

@Composable
fun ButtonGroupWithSeparator() {
    ButtonGroup {

Plain

plain buttons, which stay unjoined.

iOS
Button Group (plain) on iOS in light modeButton Group (plain) on iOS in dark mode
import { ArrowLeftIcon, ArrowRightIcon, ShareIcon } from "phosphor-react-native"
import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"
import { Icon } from "@/components/ui/icon"
import { Text } from "@/components/ui/text"

export function ButtonGroupPlainExample() {
  return (
    <ButtonGroup>
      <Button variant="plain">
        <Icon as={ArrowLeftIcon} />
        <Text>Back</Text>
      </Button>
      <Button variant="plain">
        <Text>Forward</Text>
        <Icon as={ArrowRightIcon} />
import EsuissUI
import PhosphorSwift
import SwiftUI

struct ButtonGroupPlainExample: View {
  var body: some View {
    ButtonGroup {
      Button {} label: {
        Label { Text("Back") } icon: { Ph.arrowLeft.regular }
      }
      .buttonStyle(.variant(.plain))
      Button {} label: {
        HStack(spacing: 6) {
          Text("Forward")
          Ph.arrowRight.regular
            .frame(width: VariantButtonStyle.Size.default.iconSize, height: VariantButtonStyle.Size.default.iconSize)
import androidx.compose.runtime.Composable
import androidx.compose.ui.semantics.contentDescription
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.ArrowLeft
import com.adamglin.phosphoricons.regular.ArrowRight
import com.adamglin.phosphoricons.regular.Share
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonGroup
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Text

@Composable
fun ButtonGroupPlain() {
    ButtonGroup {

Split button

A main action with a joined icon button for more options.

iOS
Button Group (split button) on iOS in light modeButton Group (split button) on iOS in dark mode
import { CaretDownIcon } from "phosphor-react-native"
import { Button } from "@/components/ui/button"
import { ButtonGroup, ButtonGroupSeparator } from "@/components/ui/button-group"
import { Icon } from "@/components/ui/icon"
import { Text } from "@/components/ui/text"

export function ButtonGroupSplitButtonExample() {
  return (
    <ButtonGroup>
      <Button>
        <Text>Publish</Text>
      </Button>
      <ButtonGroupSeparator className="bg-primary-foreground/30" />
      <Button size="icon" accessibilityLabel="More options">
        <Icon as={CaretDownIcon} />
      </Button>
import EsuissUI
import PhosphorSwift
import SwiftUI

struct ButtonGroupSplitButtonExample: View {
  var body: some View {
    ButtonGroup {
      Button("Publish") {}.buttonStyle(.variant())
      ButtonGroupSeparator()
      ButtonGroupIconButton(icon: Ph.caretDown.regular, label: "More options", variant: .default)
    }
  }
}

struct ButtonGroupIcon: View {
  let icon: Image
import androidx.compose.runtime.Composable
import androidx.compose.ui.semantics.contentDescription
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.CaretDown
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonGroup
import com.esuiss.ui.components.ButtonGroupSeparator
import com.esuiss.ui.components.ButtonSize
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Icon
import com.esuiss.ui.theme.EsuissTheme

@Composable
fun ButtonGroupIconButton(
    label: String,

With input

An input joined to a search button.

iOS
Button Group (with input) on iOS in light modeButton Group (with input) on iOS in dark mode
import { MagnifyingGlassIcon } from "phosphor-react-native"
import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"
import { Icon } from "@/components/ui/icon"
import { Input } from "@/components/ui/input"

export function ButtonGroupWithInputExample() {
  return (
    <ButtonGroup className="self-stretch">
      <Input
        accessibilityLabel="Search"
        placeholder="Search..."
        className="w-auto flex-1"
      />
      <Button variant="secondary" size="icon-lg" accessibilityLabel="Search">
        <Icon as={MagnifyingGlassIcon} />
import EsuissUI
import PhosphorSwift
import SwiftUI

struct ButtonGroupIcon: View {
  let icon: Image
  let size: VariantButtonStyle.Size

  var body: some View {
    icon.frame(width: size.iconSize, height: size.iconSize)
  }
}

struct ButtonGroupIconButton: View {
  let icon: Image
  let label: LocalizedStringKey
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.MagnifyingGlass
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonGroup
import com.esuiss.ui.components.ButtonSize
import com.esuiss.ui.components.ButtonVariant

With input group

An input group joined with buttons.

iOS
Button Group (with input group) on iOS in light modeButton Group (with input group) on iOS in dark mode
import { MicrophoneIcon, PlusIcon } from "phosphor-react-native"
import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"
import { Icon } from "@/components/ui/icon"
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@/components/ui/input-group"

export function ButtonGroupWithInputGroupExample() {
  return (
    <ButtonGroup className="self-stretch">
      <ButtonGroup>
        <Button variant="outline" size="icon-lg" accessibilityLabel="Add">
          <Icon as={PlusIcon} />
        </Button>
      </ButtonGroup>
      <ButtonGroup className="flex-1">
        <InputGroup className="flex-1">
import EsuissUI
import PhosphorSwift
import SwiftUI

struct ButtonGroupIcon: View {
  let icon: Image
  let size: VariantButtonStyle.Size

  var body: some View {
    icon.frame(width: size.iconSize, height: size.iconSize)
  }
}

struct ButtonGroupIconButton: View {
  let icon: Image
  let label: LocalizedStringKey
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.Microphone
import com.adamglin.phosphoricons.regular.Plus

With text

A ButtonGroupText URL prefix joined to an input and a save button.

iOS
Button Group (with text) on iOS in light modeButton Group (with text) on iOS in dark mode
import { LinkIcon } from "phosphor-react-native"
import { Button } from "@/components/ui/button"
import { ButtonGroup, ButtonGroupText } from "@/components/ui/button-group"
import { Icon } from "@/components/ui/icon"
import { Input } from "@/components/ui/input"
import { Text } from "@/components/ui/text"

export function ButtonGroupWithTextExample() {
  return (
    <ButtonGroup className="self-stretch">
      <ButtonGroupText>
        <Icon as={LinkIcon} />
        <Text>https://</Text>
      </ButtonGroupText>
      <Input
        accessibilityLabel="Website"
import EsuissUI
import PhosphorSwift
import SwiftUI

struct ButtonGroupWithTextDemo: View {
  @State private var url = ""

  var body: some View {
    ButtonGroup {
      ButtonGroupText {
        Label { Text(verbatim: "https://") } icon: { Ph.link.regular }
      }
      TextField("Website", text: $url, prompt: Text(verbatim: "example.com"))
        .textFieldStyle(.input)
        .keyboardType(.URL)
        .textInputAutocapitalization(.never)
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.Link
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonGroup
import com.esuiss.ui.components.ButtonGroupText
import com.esuiss.ui.components.ButtonSize

With dropdown menu

A button joined to a dropdown menu trigger.

iOS
Button Group (with dropdown menu) on iOS in light modeButton Group (with dropdown menu) on iOS in dark mode
import { CaretDownIcon, CopyIcon, ShareIcon, UserCircleMinusIcon, WarningCircleIcon } from "phosphor-react-native"
import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Icon } from "@/components/ui/icon"
import { Text } from "@/components/ui/text"

export function ButtonGroupWithDropdownMenuExample() {
  return (
    <ButtonGroup>
      <Button variant="outline">
        <Text>Follow</Text>
      </Button>
      <DropdownMenu>
        <DropdownMenuTrigger
          accessibilityLabel="More options"
import EsuissUI
import PhosphorSwift
import SwiftUI

struct ButtonGroupWithDropdownMenuExample: View {
  var body: some View {
    ButtonGroup {
      Button("Follow") {}.buttonStyle(.variant(.outline))
      DropdownMenu(align: .end) {
        DropdownMenuGroup {
          DropdownMenuItem("Mute conversation", icon: Ph.warningCircle.regular) {}
          DropdownMenuItem("Copy link", icon: Ph.copy.regular) {}
          DropdownMenuItem("Share conversation", icon: Ph.share.regular) {}
        }
        DropdownMenuSeparator()
        DropdownMenuGroup {
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.semantics.contentDescription
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.CaretDown
import com.adamglin.phosphoricons.regular.Copy
import com.adamglin.phosphoricons.regular.Share
import com.adamglin.phosphoricons.regular.UserCircleMinus
import com.adamglin.phosphoricons.regular.WarningCircle
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonGroup
import com.esuiss.ui.components.ButtonSize

With select

A select joined to an input for a currency amount.

iOS
Button Group (with select) on iOS in light modeButton Group (with select) on iOS in dark mode
import { ArrowRightIcon } from "phosphor-react-native"
import * as React from "react"
import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"
import { Icon } from "@/components/ui/icon"
import { Input } from "@/components/ui/input"
import { type Option, Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"

const currencies: Option[] = [
  { value: "usd", label: "$" },
  { value: "eur", label: "€" },
  { value: "gbp", label: "£" },
  { value: "try", label: "₺" },
]

function CurrencyExample() {
import EsuissUI
import PhosphorSwift
import SwiftUI

struct ButtonGroupIcon: View {
  let icon: Image
  let size: VariantButtonStyle.Size

  var body: some View {
    icon.frame(width: size.iconSize, height: size.iconSize)
  }
}

struct ButtonGroupIconButton: View {
  let icon: Image
  let label: LocalizedStringKey
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular

With popover

A button joined to a popover trigger.

iOS
Button Group (with popover) on iOS in light modeButton Group (with popover) on iOS in dark mode
import { CaretDownIcon, SparkleIcon } from "phosphor-react-native"
import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"
import { Icon } from "@/components/ui/icon"
import { Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger } from "@/components/ui/popover"
import { Text } from "@/components/ui/text"
import { Textarea } from "@/components/ui/textarea"

export function ButtonGroupWithPopoverExample() {
  return (
    <ButtonGroup>
      <Button variant="outline">
        <Icon as={SparkleIcon} />
        <Text>Assistant</Text>
      </Button>
      <Popover>
import EsuissUI
import PhosphorSwift
import SwiftUI

struct ButtonGroupIcon: View {
  let icon: Image
  let size: VariantButtonStyle.Size

  var body: some View {
    icon.frame(width: size.iconSize, height: size.iconSize)
  }
}

struct ButtonGroupPopoverDemo: View {
  @State private var open = false
  @State private var task = ""
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.CaretDown
import com.adamglin.phosphoricons.regular.Sparkle
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonGroup

Pagination

Page buttons with previous and next arrows.

iOS
Button Group (pagination) on iOS in light modeButton Group (pagination) on iOS in dark mode
import { CaretLeftIcon, CaretRightIcon } from "phosphor-react-native"
import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"
import { Icon } from "@/components/ui/icon"
import { Text } from "@/components/ui/text"

export function ButtonGroupPaginationExample() {
  return (
    <ButtonGroup accessibilityLabel="Pagination">
      <Button variant="outline" size="icon" accessibilityLabel="Previous page">
        <Icon as={CaretLeftIcon} />
      </Button>
      <Button variant="outline">
        <Text>1</Text>
      </Button>
      <Button variant="outline" accessibilityState={{ selected: true }}>
import EsuissUI
import PhosphorSwift
import SwiftUI

struct ButtonGroupPaginationExample: View {
  var body: some View {
    ButtonGroup {
      ButtonGroupIconButton(icon: Ph.caretLeft.regular, label: "Previous page", variant: .outline)
      Button {} label: { Text(verbatim: "1") }.buttonStyle(.variant(.outline))
      Button {} label: { Text(verbatim: "2") }
        .buttonStyle(.variant(.outline))
        .accessibilityAddTraits(.isSelected)
      Button {} label: { Text(verbatim: "3") }.buttonStyle(.variant(.outline))
      ButtonGroupIconButton(icon: Ph.caretRight.regular, label: "Next page", variant: .outline)
    }
    .accessibilityLabel(Text("Pagination"))
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.selected
import androidx.compose.ui.semantics.semantics
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.CaretLeft
import com.adamglin.phosphoricons.regular.CaretRight
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonGroup
import com.esuiss.ui.components.ButtonSize
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Icon

@Composable

API Reference

01

ButtonGroup

Container that joins its children.

PropTypeDefaultDescription
orientation"horizontal" | "vertical""horizontal"horizontal or vertical. Defaults to horizontal.

Also accepts the props of ViewProps.

02

ButtonGroupText

Pill-shaped label or icon slot.

PropTypeDefaultDescription
renderRenderProp<Record<string, unknown>, Record<string, never>>-Element to render instead of the default View.

Also accepts the props of ViewProps.

03

ButtonGroupSeparator

Divider between joined buttons.

Also accepts the props of React.ComponentProps<typeof Separator>.

Accessibility

  • The container has the group role.
  • Each button keeps its own role and state.
  • Icon-only buttons need an accessibilityLabel.

Notes

  • Uses bg-secondary for ButtonGroupText and bg-foreground/20 for separators; light and dark follow the system appearance.
NextCalendarPreviousButton

On this page

  1. 01Installation
    • CLI
    • Manual
  2. 02Usage
  3. 03Examples
    • Outline
    • Orientation
    • Sizes
    • Nested
    • With separator
    • Plain
    • Split button
    • With input
    • With input group
    • With text
    • With dropdown menu
    • With select
    • With popover
    • Pagination
  4. 04API Reference
    • ButtonGroup
    • ButtonGroupText
    • ButtonGroupSeparator
  5. 05Accessibility
  6. 06Notes
  7. 07Installation
    • Manual
  8. 08Usage
  9. 09Examples
    • Outline
    • Orientation
    • Sizes
    • Nested
    • With separator
    • Plain
    • Split button
    • With input
    • With input group
    • With text
    • With dropdown menu
    • With select
    • With popover
    • Pagination
  10. 10API Reference
    • ButtonGroup
    • ButtonGroupText
    • ButtonGroupSeparator
  11. 11Accessibility
  12. 12Notes