suiss/ui
  • Docs
  • Components
  • Patterns
  • Interactions
  • Charts
  • Blocks
GitHubGet started
DocsComponentsPatternsInteractionsChartsBlocks
Components82
  • 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
  • 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
  • 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.

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:

  1. Update the import paths to match your project.

Usage

import { ButtonGroup, ButtonGroupText, ButtonGroupSeparator } 
<





























































<























































<





















































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.

Orientation

Stacks zoom buttons vertically with orientation="vertical".

Sizes

Groups of sm, default and lg buttons.

Nested

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

With separator

Divides two secondary buttons with ButtonGroupSeparator.

Plain

Groups plain buttons with leading and trailing icons.

Split button

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

With input

Attaches a search button to an Input.

With input group

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

With text

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

With dropdown menu

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

With select

Joins a currency Select to an amount input.

With popover

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

Pagination

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

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.

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
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
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>
$ 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:

  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:

  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:

  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 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: 24
      case .sm, .iconSm: 32
      case .default, .icon: 36
      case .lg, .iconLg: 44
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 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.text.TextStyle
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
kotlin/theme/Tokens.kt
package com.esuiss.ui.theme

import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
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,
    val primaryForeground: 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>
          <Icon as={ArchiveIcon} className="size-4" />
          <Text>Archive</Text>
        </DropdownMenuItem>
      </DropdownMenuGroup>
      <DropdownMenuSeparator />
      <DropdownMenuGroup>
        <DropdownMenuItem>
          <Icon as={ClockIcon} className="size-4" />
          <Text>Snooze</Text>
        </DropdownMenuItem>
        <DropdownMenuItem>
          <Icon as={CalendarPlusIcon} className="size-4" />
          <Text>Add to calendar</Text>
        </DropdownMenuItem>
        <DropdownMenuItem>
          <Icon as={TagIcon} className="size-4" />
          <Text>Label as</Text>
        </DropdownMenuItem>
      </DropdownMenuGroup>
      <DropdownMenuSeparator />
      <DropdownMenuGroup>
        <DropdownMenuItem variant="destructive">
          <Icon as={TrashIcon} className="size-4" />
          <Text>Trash</Text>
        </DropdownMenuItem>
      </DropdownMenuGroup>
    </DropdownMenuContent>
  )
}

export function ButtonGroupOutlineExample() {
  return (
    <ButtonGroup>
      <ButtonGroup>
        <Button variant="outline" size="icon" accessibilityLabel="Go back">
          <Icon as={ArrowLeftIcon} />
        </Button>
      </ButtonGroup>
      <ButtonGroup>
        <Button variant="outline">
          <Text>Archive</Text>
        </Button>
        <Button variant="outline">
          <Text>Report</Text>
        </Button>
      </ButtonGroup>
      <ButtonGroup>
        <Button variant="outline">
          <Text>Snooze</Text>
        </Button>
        <DropdownMenu>
          <DropdownMenuTrigger
            accessibilityLabel="More options"
            render={<Button variant="outline" size="icon" />}
          >
            <Icon as={DotsThreeIcon} />
          </DropdownMenuTrigger>
          <MailMenuContent />
        </DropdownMenu>
      </ButtonGroup>
    </ButtonGroup>
  )
}
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))
        ButtonGroupMailMenu(variant: .outline)
      }
    }
  }
}

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
  let variant: VariantButtonStyle.Variant
  var size: VariantButtonStyle.Size = .icon

  var body: some View {
    Button {} label: {
      ButtonGroupIcon(icon: icon, size: size)
    }
    .buttonStyle(.variant(variant, size: size))
    .accessibilityLabel(Text(label))
  }
}

struct ButtonGroupMailMenu: View {
  let variant: VariantButtonStyle.Variant

  var body: some View {
    DropdownMenu(align: .end) {
      DropdownMenuGroup {
        DropdownMenuItem("Mark as read", icon: Ph.listPlus.regular) {}
        DropdownMenuItem("Archive", icon: Ph.archive.regular) {}
      }
      DropdownMenuSeparator()
      DropdownMenuGroup {
        DropdownMenuItem("Snooze", icon: Ph.clock.regular) {}
        DropdownMenuItem("Add to calendar", icon: Ph.calendarPlus.regular) {}
        DropdownMenuItem("Label as", icon: Ph.tag.regular) {}
      }
      DropdownMenuSeparator()
      DropdownMenuGroup {
        DropdownMenuItem("Trash", icon: Ph.trash.regular, variant: .destructive) {}
      }
    } label: {
      ButtonGroupIcon(icon: Ph.dotsThree.bold, size: .icon)
    }
    .buttonStyle(.variant(variant, size: .icon))
    .accessibilityLabel(Text("More options"))
  }
}
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
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.DropdownMenu
import com.esuiss.ui.components.DropdownMenuGroup
import com.esuiss.ui.components.DropdownMenuItem
import com.esuiss.ui.components.DropdownMenuSeparator
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.MenuItemVariant
import com.esuiss.ui.components.PopupAlign

@Composable
fun ButtonGroupOutlineExample() {
    ButtonGroupMail(ButtonVariant.Outline, separators = false)
}

@Composable
fun ButtonGroupIconButton(
    label: String,
    icon: androidx.compose.ui.graphics.vector.ImageVector,
    variant: ButtonVariant = ButtonVariant.Default,
    size: ButtonSize = ButtonSize.Icon,
    onClick: () -> Unit = {},
) {
    Button(onClick = onClick, variant = variant, size = size) {
        Icon(icon, contentDescription = label)
    }
}

@Composable
fun ButtonGroupMailMenu(variant: ButtonVariant) {
    var open by rememberSaveable { mutableStateOf(false) }
    DropdownMenu(
        expanded = open,
        onExpandedChange = { open = it },
        trigger = { ButtonGroupIconButton("More options", PhosphorIcons.Regular.DotsThree, variant) { open = true } },
        align = PopupAlign.End,
    ) {
        DropdownMenuGroup {
            DropdownMenuItem("Mark as read", onClick = {}, icon = PhosphorIcons.Regular.ListPlus)
            DropdownMenuItem("Archive", onClick = {}, icon = PhosphorIcons.Regular.Archive)
        }
        DropdownMenuSeparator()
        DropdownMenuGroup {
            DropdownMenuItem("Snooze", onClick = {}, icon = PhosphorIcons.Regular.Clock)
            DropdownMenuItem("Add to calendar", onClick = {}, icon = PhosphorIcons.Regular.CalendarPlus)
            DropdownMenuItem("Label as", onClick = {}, icon = PhosphorIcons.Regular.Tag)
        }
        DropdownMenuSeparator()
        DropdownMenuGroup {
            DropdownMenuItem("Trash", onClick = {}, icon = PhosphorIcons.Regular.Trash, variant = MenuItemVariant.Destructive)
        }
    }
}

@Composable
fun ButtonGroupMail(variant: ButtonVariant, separators: Boolean) {
    ButtonGroup {
        ButtonGroup {
            ButtonGroupIconButton("Go back", PhosphorIcons.Regular.ArrowLeft, variant)
        }
        ButtonGroup {
            Button("Archive", onClick = {}, variant = variant)
            if (separators) ButtonGroupSeparator()
            Button("Report", onClick = {}, variant = variant)
        }
        ButtonGroup {
            Button("Snooze", onClick = {}, variant = variant)
            if (separators) ButtonGroupSeparator()
            ButtonGroupMailMenu(variant)
        }
    }
}

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 {
  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
  let variant: VariantButtonStyle.Variant
  var size: VariantButtonStyle.Size = .icon

  var body: some View {
    Button {} label: {
      ButtonGroupIcon(icon: icon, size: size)
    }
    .buttonStyle(.variant(variant, size: size))
    .accessibilityLabel(Text(label))
  }
}
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

@Composable
fun ButtonGroupIconButton(
    label: String,
    icon: androidx.compose.ui.graphics.vector.ImageVector,
    variant: ButtonVariant = ButtonVariant.Default,
    size: ButtonSize = ButtonSize.Icon,
    onClick: () -> Unit = {},
) {
    Button(onClick = onClick, variant = variant, size = size) {
        Icon(icon, contentDescription = label)
    }
}

@Composable
fun ButtonGroupOrientationStory() {
    ButtonGroup(
        modifier = Modifier.semantics { contentDescription = "Media controls" },
        orientation = ButtonGroupOrientation.Vertical,
    ) {
        ButtonGroupIconButton("Zoom in", PhosphorIcons.Regular.Plus, ButtonVariant.Secondary)
        ButtonGroupSeparator(orientation = Orientation.Horizontal)
        ButtonGroupIconButton("Zoom out", PhosphorIcons.Regular.Minus, ButtonVariant.Secondary)
    }
}

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>
        </Button>
        <Button variant="outline" size="sm">
          <Text>Group</Text>
        </Button>
        <Button variant="outline" size="icon-sm" accessibilityLabel="Add">
          <Icon as={PlusIcon} />
        </Button>
      </ButtonGroup>
      <ButtonGroup>
        <Button variant="outline">
          <Text>Default</Text>
        </Button>
        <Button variant="outline">
          <Text>Button</Text>
        </Button>
        <Button variant="outline">
          <Text>Group</Text>
        </Button>
        <Button variant="outline" size="icon" accessibilityLabel="Add">
          <Icon as={PlusIcon} />
        </Button>
      </ButtonGroup>
      <ButtonGroup>
        <Button variant="outline" size="lg">
          <Text>Large</Text>
        </Button>
        <Button variant="outline" size="lg">
          <Text>Button</Text>
        </Button>
        <Button variant="outline" size="lg">
          <Text>Group</Text>
        </Button>
        <Button variant="outline" size="icon-lg" accessibilityLabel="Add">
          <Icon as={PlusIcon} />
        </Button>
      </ButtonGroup>
    </View>
  )
}
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
  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
  let variant: VariantButtonStyle.Variant
  var size: VariantButtonStyle.Size = .icon

  var body: some View {
    Button {} label: {
      ButtonGroupIcon(icon: icon, size: size)
    }
    .buttonStyle(.variant(variant, size: size))
    .accessibilityLabel(Text(label))
  }
}

struct ButtonGroupSizeRow: View {
  let title: LocalizedStringKey
  let size: VariantButtonStyle.Size
  let iconSize: VariantButtonStyle.Size

  var body: some View {
    ButtonGroup {
      Button(title) {}.buttonStyle(.variant(.outline, size: size))
      Button("Button") {}.buttonStyle(.variant(.outline, size: size))
      Button("Group") {}.buttonStyle(.variant(.outline, size: size))
      ButtonGroupIconButton(icon: Ph.plus.regular, label: "Add", variant: .outline, size: iconSize)
    }
  }
}
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(
    label: String,
    icon: androidx.compose.ui.graphics.vector.ImageVector,
    variant: ButtonVariant = ButtonVariant.Default,
    size: ButtonSize = ButtonSize.Icon,
    onClick: () -> Unit = {},
) {
    Button(onClick = onClick, variant = variant, size = size) {
        Icon(icon, contentDescription = label)
    }
}

@Composable
fun ButtonGroupSizes() {
    Column(verticalArrangement = Arrangement.spacedBy(24.dp)) {
        listOf(
            Triple("Small", ButtonSize.Sm, ButtonSize.IconSm),
            Triple("Default", ButtonSize.Default, ButtonSize.Icon),
            Triple("Large", ButtonSize.Lg, ButtonSize.IconLg),
        ).forEach { (label, size, icon) ->
            ButtonGroup {
                Button(label, onClick = {}, variant = ButtonVariant.Outline, size = size)
                Button("Button", onClick = {}, variant = ButtonVariant.Outline, size = size)
                Button("Group", onClick = {}, variant = ButtonVariant.Outline, size = size)
                ButtonGroupIconButton("Add", PhosphorIcons.Regular.Plus, ButtonVariant.Outline, icon)
            }
        }
    }
}

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>
      <ButtonGroup>
        <Button variant="outline" size="icon-sm" accessibilityLabel="Previous">
          <Icon as={ArrowLeftIcon} />
        </Button>
        <Button variant="outline" size="icon-sm" accessibilityLabel="Next">
          <Icon as={ArrowRightIcon} />
        </Button>
      </ButtonGroup>
    </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)
      }
    }
  }
}

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
  let variant: VariantButtonStyle.Variant
  var size: VariantButtonStyle.Size = .icon

  var body: some View {
    Button {} label: {
      ButtonGroupIcon(icon: icon, size: size)
    }
    .buttonStyle(.variant(variant, size: size))
    .accessibilityLabel(Text(label))
  }
}
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,
    variant: ButtonVariant = ButtonVariant.Default,
    size: ButtonSize = ButtonSize.Icon,
    onClick: () -> Unit = {},
) {
    Button(onClick = onClick, variant = variant, size = size) {
        Icon(icon, contentDescription = label)
    }
}

@Composable
fun ButtonGroupNested() {
    ButtonGroup {
        ButtonGroup {
            listOf("1", "2", "3", "4", "5").forEach { page ->
                Button(page, onClick = {}, variant = ButtonVariant.Outline, size = ButtonSize.Sm)
            }
        }
        ButtonGroup {
            ButtonGroupIconButton("Previous", PhosphorIcons.Regular.ArrowLeft, ButtonVariant.Outline, ButtonSize.IconSm)
            ButtonGroupIconButton("Next", PhosphorIcons.Regular.ArrowRight, ButtonVariant.Outline, ButtonSize.IconSm)
        }
    }
}

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} />
        <Text>Share</Text>
      </Button>
    </ButtonGroup>
  )
}
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 {
        Button(onClick = {}, variant = ButtonVariant.Secondary) {
            Icon(PhosphorIcons.Regular.Copy, contentDescription = null)
            Text("Copy")
        }
        ButtonGroupSeparator()
        Button(onClick = {}, variant = ButtonVariant.Secondary) {
            Icon(PhosphorIcons.Regular.Share, contentDescription = null)
            Text("Share")
        }
    }
}

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} />
      </Button>
      <Button variant="plain">
        <Icon as={ShareIcon} />
        <Text>Share</Text>
      </Button>
    </ButtonGroup>
  )
}
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)
        }
      }
      .buttonStyle(.variant(.plain))
      Button {} label: {
        Label { Text("Share") } icon: { Ph.share.regular }
      }
      .buttonStyle(.variant(.plain))
    }
  }
}
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 {
        Button(onClick = {}, variant = ButtonVariant.Plain) {
            Icon(PhosphorIcons.Regular.ArrowLeft, contentDescription = null)
            Text("Back")
        }
        Button(onClick = {}, variant = ButtonVariant.Plain) {
            Text("Forward")
            Icon(PhosphorIcons.Regular.ArrowRight, contentDescription = null)
        }
        Button(onClick = {}, variant = ButtonVariant.Plain) {
            Icon(PhosphorIcons.Regular.Share, contentDescription = null)
            Text("Share")
        }
    }
}

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>
    </ButtonGroup>
  )
}
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
  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
  let variant: VariantButtonStyle.Variant
  var size: VariantButtonStyle.Size = .icon

  var body: some View {
    Button {} label: {
      ButtonGroupIcon(icon: icon, size: size)
    }
    .buttonStyle(.variant(variant, size: size))
    .accessibilityLabel(Text(label))
  }
}
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,
    icon: androidx.compose.ui.graphics.vector.ImageVector,
    variant: ButtonVariant = ButtonVariant.Default,
    size: ButtonSize = ButtonSize.Icon,
    onClick: () -> Unit = {},
) {
    Button(onClick = onClick, variant = variant, size = size) {
        Icon(icon, contentDescription = label)
    }
}

@Composable
fun ButtonGroupSplitButton() {
    ButtonGroup {
        Button("Publish", onClick = {})
        ButtonGroupSeparator(color = EsuissTheme.colors.primaryForeground.copy(alpha = 0.3f))
        ButtonGroupIconButton("More options", PhosphorIcons.Regular.CaretDown)
    }
}

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} />
      </Button>
    </ButtonGroup>
  )
}
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
  let variant: VariantButtonStyle.Variant
  var size: VariantButtonStyle.Size = .icon

  var body: some View {
    Button {} label: {
      ButtonGroupIcon(icon: icon, size: size)
    }
    .buttonStyle(.variant(variant, size: size))
    .accessibilityLabel(Text(label))
  }
}

struct ButtonGroupWithInputDemo: View {
  @State private var query = ""

  var body: some View {
    ButtonGroup {
      TextField("Search", text: $query, prompt: Text("Search..."))
        .textFieldStyle(.input)
      ButtonGroupIconButton(icon: Ph.magnifyingGlass.regular, label: "Search", variant: .secondary, size: .iconLg)
    }
  }
}
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
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Input

@Composable
fun ButtonGroupIconButton(
    label: String,
    icon: androidx.compose.ui.graphics.vector.ImageVector,
    variant: ButtonVariant = ButtonVariant.Default,
    size: ButtonSize = ButtonSize.Icon,
    onClick: () -> Unit = {},
) {
    Button(onClick = onClick, variant = variant, size = size) {
        Icon(icon, contentDescription = label)
    }
}

@Composable
fun ButtonGroupWithInput() {
    var search by rememberSaveable { mutableStateOf("") }
    ButtonGroup(Modifier.fillMaxWidth()) {
        Input(
            search,
            { search = it },
            Modifier.semantics { contentDescription = "Search" },
            placeholder = "Search...",
        )
        ButtonGroupIconButton("Search", PhosphorIcons.Regular.MagnifyingGlass, ButtonVariant.Secondary, ButtonSize.IconLg)
    }
}

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">
          <InputGroupInput
            accessibilityLabel="Message"
            placeholder="Send a message..."
          />
          <InputGroupAddon align="inline-end">
            <InputGroupButton size="icon-xs" accessibilityLabel="Voice mode">
              <Icon as={MicrophoneIcon} />
            </InputGroupButton>
          </InputGroupAddon>
        </InputGroup>
      </ButtonGroup>
    </ButtonGroup>
  )
}
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
  let variant: VariantButtonStyle.Variant
  var size: VariantButtonStyle.Size = .icon

  var body: some View {
    Button {} label: {
      ButtonGroupIcon(icon: icon, size: size)
    }
    .buttonStyle(.variant(variant, size: size))
    .accessibilityLabel(Text(label))
  }
}

struct ButtonGroupWithInputGroupDemo: View {
  @State private var message = ""

  var body: some View {
    ButtonGroup {
      ButtonGroup {
        ButtonGroupIconButton(icon: Ph.plus.regular, label: "Add", variant: .outline, size: .iconLg)
      }
      ButtonGroup {
        InputGroup {
          InputGroupInput("Send a message...", text: $message)
            .accessibilityLabel(Text("Message"))
          InputGroupAddon(align: .inlineEnd) {
            InputGroupButton(size: .iconXs) {} label: {
              Ph.microphone.regular.frame(width: 14, height: 14)
            }
            .accessibilityLabel(Text("Voice mode"))
          }
        }
      }
    }
  }
}
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
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
import com.esuiss.ui.components.InputGroup
import com.esuiss.ui.components.InputGroupAddon
import com.esuiss.ui.components.InputGroupAddonAlign
import com.esuiss.ui.components.InputGroupButton
import com.esuiss.ui.components.InputGroupButtonSize
import com.esuiss.ui.components.InputGroupInput

@Composable
fun ButtonGroupIconButton(
    label: String,
    icon: androidx.compose.ui.graphics.vector.ImageVector,
    variant: ButtonVariant = ButtonVariant.Default,
    size: ButtonSize = ButtonSize.Icon,
    onClick: () -> Unit = {},
) {
    Button(onClick = onClick, variant = variant, size = size) {
        Icon(icon, contentDescription = label)
    }
}

@Composable
fun ButtonGroupWithInputGroup() {
    var message by rememberSaveable { mutableStateOf("") }
    BoxWithConstraints(Modifier.fillMaxWidth()) {
        val rest = maxWidth - 8.dp - ButtonSize.IconLg.height
        ButtonGroup(Modifier.fillMaxWidth()) {
            ButtonGroup {
                ButtonGroupIconButton("Add", PhosphorIcons.Regular.Plus, ButtonVariant.Outline, ButtonSize.IconLg)
            }
            ButtonGroup(Modifier.width(rest)) {
                InputGroup(Modifier.width(rest)) {
                    InputGroupInput(
                        message,
                        { message = it },
                        Modifier.semantics { contentDescription = "Message" },
                        placeholder = "Send a message...",
                    )
                    InputGroupAddon(align = InputGroupAddonAlign.InlineEnd) {
                        InputGroupButton(onClick = {}, size = InputGroupButtonSize.IconXs) {
                            Icon(PhosphorIcons.Regular.Microphone, contentDescription = "Voice mode")
                        }
                    }
                }
            }
        }
    }
}

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"
        placeholder="example.com"
        className="w-auto flex-1"
      />
      <Button variant="secondary" size="lg">
        <Text>Save</Text>
      </Button>
    </ButtonGroup>
  )
}
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)
      Button("Save") {}.buttonStyle(.variant(.secondary, size: .lg))
    }
  }
}
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
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Input
import com.esuiss.ui.components.Text

@Composable
fun ButtonGroupWithText() {
    var website by rememberSaveable { mutableStateOf("") }
    ButtonGroup(Modifier.fillMaxWidth()) {
        ButtonGroupText {
            Icon(PhosphorIcons.Regular.Link, contentDescription = null)
            Text("https://")
        }
        Input(
            website,
            { website = it },
            Modifier.semantics { contentDescription = "Website" },
            placeholder = "example.com",
        )
        Button("Save", onClick = {}, variant = ButtonVariant.Secondary, size = ButtonSize.Lg)
    }
}

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"
          render={<Button variant="outline" size="icon" />}
        >
          <Icon as={CaretDownIcon} />
        </DropdownMenuTrigger>
        <DropdownMenuContent align="end" className="w-56">
          <DropdownMenuGroup>
            <DropdownMenuItem>
              <Icon as={WarningCircleIcon} className="size-4" />
              <Text>Mute conversation</Text>
            </DropdownMenuItem>
            <DropdownMenuItem>
              <Icon as={CopyIcon} className="size-4" />
              <Text>Copy link</Text>
            </DropdownMenuItem>
            <DropdownMenuItem>
              <Icon as={ShareIcon} className="size-4" />
              <Text>Share conversation</Text>
            </DropdownMenuItem>
          </DropdownMenuGroup>
          <DropdownMenuSeparator />
          <DropdownMenuGroup>
            <DropdownMenuItem variant="destructive">
              <Icon as={UserCircleMinusIcon} className="size-4" />
              <Text>Unfollow</Text>
            </DropdownMenuItem>
          </DropdownMenuGroup>
        </DropdownMenuContent>
      </DropdownMenu>
    </ButtonGroup>
  )
}
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 {
          DropdownMenuItem("Unfollow", icon: Ph.userCircleMinus.regular, variant: .destructive) {}
        }
      } label: {
        ButtonGroupIcon(icon: Ph.caretDown.regular, size: .icon)
      }
      .buttonStyle(.variant(.outline, size: .icon))
      .accessibilityLabel(Text("More options"))
    }
  }
}

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

  var body: some View {
    icon.frame(width: size.iconSize, height: size.iconSize)
  }
}
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
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.DropdownMenu
import com.esuiss.ui.components.DropdownMenuGroup
import com.esuiss.ui.components.DropdownMenuItem
import com.esuiss.ui.components.DropdownMenuSeparator
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.MenuItemVariant
import com.esuiss.ui.components.PopupAlign

@Composable
fun ButtonGroupIconButton(
    label: String,
    icon: androidx.compose.ui.graphics.vector.ImageVector,
    variant: ButtonVariant = ButtonVariant.Default,
    size: ButtonSize = ButtonSize.Icon,
    onClick: () -> Unit = {},
) {
    Button(onClick = onClick, variant = variant, size = size) {
        Icon(icon, contentDescription = label)
    }
}

@Composable
fun ButtonGroupWithDropdownMenu() {
    var open by rememberSaveable { mutableStateOf(false) }
    ButtonGroup {
        Button("Follow", onClick = {}, variant = ButtonVariant.Outline)
        DropdownMenu(
            expanded = open,
            onExpandedChange = { open = it },
            trigger = { ButtonGroupIconButton("More options", PhosphorIcons.Regular.CaretDown, ButtonVariant.Outline) { open = true } },
            align = PopupAlign.End,
        ) {
            DropdownMenuGroup {
                DropdownMenuItem("Mute conversation", onClick = {}, icon = PhosphorIcons.Regular.WarningCircle)
                DropdownMenuItem("Copy link", onClick = {}, icon = PhosphorIcons.Regular.Copy)
                DropdownMenuItem("Share conversation", onClick = {}, icon = PhosphorIcons.Regular.Share)
            }
            DropdownMenuSeparator()
            DropdownMenuGroup {
                DropdownMenuItem(
                    "Unfollow",
                    onClick = {},
                    icon = PhosphorIcons.Regular.UserCircleMinus,
                    variant = MenuItemVariant.Destructive,
                )
            }
        }
    }
}

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() {
  const [value, setValue] = React.useState<string | null>("usd")
  return (
    <ButtonGroup className="self-stretch">
      <ButtonGroup className="flex-1">
        <Select items={currencies} value={value} onValueChange={setValue}>
          <SelectTrigger accessibilityLabel="Currency">
            <SelectValue />
          </SelectTrigger>
          <SelectContent>
            <SelectGroup>
              {currencies.map((currency) => (
                <SelectItem
                  key={currency.value}
                  value={currency.value}
                  label={currency.label}
                />
              ))}
            </SelectGroup>
          </SelectContent>
        </Select>
        <Input
          accessibilityLabel="Amount"
          placeholder="10.00"
          inputMode="decimal"
          className="w-auto flex-1"
        />
      </ButtonGroup>
      <ButtonGroup>
        <Button variant="outline" size="icon-lg" accessibilityLabel="Send">
          <Icon as={ArrowRightIcon} />
        </Button>
      </ButtonGroup>
    </ButtonGroup>
  )
}

export function ButtonGroupWithSelectExample() {
  return (
    <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
  let variant: VariantButtonStyle.Variant
  var size: VariantButtonStyle.Size = .icon

  var body: some View {
    Button {} label: {
      ButtonGroupIcon(icon: icon, size: size)
    }
    .buttonStyle(.variant(variant, size: size))
    .accessibilityLabel(Text(label))
  }
}

struct ButtonGroupCurrencyDemo: View {
  @State private var currency: String? = "usd"
  @State private var amount = ""

  var body: some View {
    ButtonGroup {
      ButtonGroup {
        SelectField(selection: $currency) {
          SelectGroup {
            SelectItem(verbatim: "$", value: "usd")
            SelectItem(verbatim: "€", value: "eur")
            SelectItem(verbatim: "£", value: "gbp")
            SelectItem(verbatim: "₺", value: "try")
          }
        }
        .frame(width: 76)
        .accessibilityLabel(Text("Currency"))
        TextField("Amount", text: $amount, prompt: Text(verbatim: "10.00"))
          .textFieldStyle(.input)
          .keyboardType(.decimalPad)
      }
      ButtonGroup {
        ButtonGroupIconButton(icon: Ph.arrowRight.regular, label: "Send", variant: .outline, size: .iconLg)
      }
    }
  }
}
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
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
import com.esuiss.ui.components.Input
import com.esuiss.ui.components.Select
import com.esuiss.ui.components.SelectOption

@Composable
fun ButtonGroupIconButton(
    label: String,
    icon: androidx.compose.ui.graphics.vector.ImageVector,
    variant: ButtonVariant = ButtonVariant.Default,
    size: ButtonSize = ButtonSize.Icon,
    onClick: () -> Unit = {},
) {
    Button(onClick = onClick, variant = variant, size = size) {
        Icon(icon, contentDescription = label)
    }
}

val buttonGroupCurrencies = listOf(
    SelectOption("usd", "$"),
    SelectOption("eur", "€"),
    SelectOption("gbp", "£"),
    SelectOption("try", "₺"),
)

@Composable
fun ButtonGroupWithSelect() {
    var currency by rememberSaveable { mutableStateOf<String?>("usd") }
    var amount by rememberSaveable { mutableStateOf("") }
    BoxWithConstraints(Modifier.fillMaxWidth()) {
        val rest = maxWidth - 8.dp - ButtonSize.IconLg.height
        ButtonGroup(Modifier.fillMaxWidth()) {
            ButtonGroup(Modifier.width(rest)) {
                Select(
                    value = currency,
                    onValueChange = { currency = it },
                    options = buttonGroupCurrencies,
                    modifier = Modifier.semantics { contentDescription = "Currency" },
                )
                Input(
                    amount,
                    { amount = it },
                    Modifier.semantics { contentDescription = "Amount" },
                    placeholder = "10.00",
                    keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
                )
            }
            ButtonGroup {
                ButtonGroupIconButton("Send", PhosphorIcons.Regular.ArrowRight, ButtonVariant.Outline, ButtonSize.IconLg)
            }
        }
    }
}

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>
        <PopoverTrigger
          accessibilityLabel="Open popover"
          render={<Button variant="outline" size="icon" />}
        >
          <Icon as={CaretDownIcon} />
        </PopoverTrigger>
        <PopoverContent align="end" className="w-80">
          <PopoverHeader>
            <PopoverTitle>Start a new task</PopoverTitle>
            <PopoverDescription>
              Describe your task in natural language.
            </PopoverDescription>
          </PopoverHeader>
          <Textarea
            accessibilityLabel="Task description"
            placeholder="I need to..."
          />
        </PopoverContent>
      </Popover>
    </ButtonGroup>
  )
}
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 = ""

  var body: some View {
    ButtonGroup {
      Button {} label: {
        Label { Text("Assistant") } icon: { Ph.sparkle.regular }
      }
      .buttonStyle(.variant(.outline))
      Button { open = true } label: {
        ButtonGroupIcon(icon: Ph.caretDown.regular, size: .icon)
      }
      .buttonStyle(.variant(.outline, size: .icon))
      .accessibilityLabel(Text("Open popover"))
      .popover(isPresented: $open) {
        PopoverContent(width: 320) {
          PopoverHeader {
            PopoverTitle("Start a new task")
            PopoverDescription("Describe your task in natural language.")
          }
          TextField("Task description", text: $task, prompt: Text("I need to..."), axis: .vertical)
            .textFieldStyle(.textarea)
        }
      }
    }
  }
}
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
import com.esuiss.ui.components.ButtonSize
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Popover
import com.esuiss.ui.components.PopoverDescription
import com.esuiss.ui.components.PopoverHeader
import com.esuiss.ui.components.PopoverTitle
import com.esuiss.ui.components.PopupAlign
import com.esuiss.ui.components.Text
import com.esuiss.ui.components.Textarea

@Composable
fun ButtonGroupIconButton(
    label: String,
    icon: androidx.compose.ui.graphics.vector.ImageVector,
    variant: ButtonVariant = ButtonVariant.Default,
    size: ButtonSize = ButtonSize.Icon,
    onClick: () -> Unit = {},
) {
    Button(onClick = onClick, variant = variant, size = size) {
        Icon(icon, contentDescription = label)
    }
}

@Composable
fun ButtonGroupWithPopover() {
    var open by rememberSaveable { mutableStateOf(false) }
    var task by rememberSaveable { mutableStateOf("") }
    ButtonGroup {
        Button(onClick = {}, variant = ButtonVariant.Outline) {
            Icon(PhosphorIcons.Regular.Sparkle, contentDescription = null)
            Text("Assistant")
        }
        Popover(
            open = open,
            onOpenChange = { open = it },
            trigger = { ButtonGroupIconButton("Open popover", PhosphorIcons.Regular.CaretDown, ButtonVariant.Outline) { open = true } },
            modifier = Modifier.width(320.dp),
            align = PopupAlign.End,
        ) {
            PopoverHeader {
                PopoverTitle("Start a new task")
                PopoverDescription("Describe your task in natural language.")
            }
            Textarea(
                task,
                { task = it },
                Modifier.semantics { contentDescription = "Task description" },
                placeholder = "I need to...",
            )
        }
    }
}

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 }}>
        <Text>2</Text>
      </Button>
      <Button variant="outline">
        <Text>3</Text>
      </Button>
      <Button variant="outline" size="icon" accessibilityLabel="Next page">
        <Icon as={CaretRightIcon} />
      </Button>
    </ButtonGroup>
  )
}
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"))
  }
}

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
  let variant: VariantButtonStyle.Variant
  var size: VariantButtonStyle.Size = .icon

  var body: some View {
    Button {} label: {
      ButtonGroupIcon(icon: icon, size: size)
    }
    .buttonStyle(.variant(variant, size: size))
    .accessibilityLabel(Text(label))
  }
}
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
fun ButtonGroupIconButton(
    label: String,
    icon: androidx.compose.ui.graphics.vector.ImageVector,
    variant: ButtonVariant = ButtonVariant.Default,
    size: ButtonSize = ButtonSize.Icon,
    onClick: () -> Unit = {},
) {
    Button(onClick = onClick, variant = variant, size = size) {
        Icon(icon, contentDescription = label)
    }
}

@Composable
fun ButtonGroupPagination() {
    ButtonGroup(Modifier.semantics { contentDescription = "Pagination" }) {
        ButtonGroupIconButton("Previous page", PhosphorIcons.Regular.CaretLeft, ButtonVariant.Outline)
        Button("1", onClick = {}, variant = ButtonVariant.Outline)
        Button("2", onClick = {}, modifier = Modifier.semantics { selected = true }, variant = ButtonVariant.Outline)
        Button("3", onClick = {}, variant = ButtonVariant.Outline)
        ButtonGroupIconButton("Next page", PhosphorIcons.Regular.CaretRight, ButtonVariant.Outline)
    }
}

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.
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>
          <Icon as={ArchiveIcon} className="size-4" />
          <Text>Archive</Text>
        </DropdownMenuItem>
      </DropdownMenuGroup>
      <DropdownMenuSeparator />
      <DropdownMenuGroup>
        <DropdownMenuItem>
          <Icon as={ClockIcon} className="size-4" />
          <Text>Snooze</Text>
        </DropdownMenuItem>
        <DropdownMenuItem>
          <Icon as={CalendarPlusIcon} className="size-4" />
          <Text>Add to calendar</Text>
        </DropdownMenuItem>
        <DropdownMenuItem>
          <Icon as={TagIcon} className="size-4" />
          <Text>Label as</Text>
        </DropdownMenuItem>
      </DropdownMenuGroup>
      <DropdownMenuSeparator />
      <DropdownMenuGroup>
        <DropdownMenuItem variant="destructive">
          <Icon as={TrashIcon} className="size-4" />
          <Text>Trash</Text>
        </DropdownMenuItem>
      </DropdownMenuGroup>
    </DropdownMenuContent>
  )
}

export function ButtonGroupExample() {
  return (
    <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>
  )
}
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 {
        Button("Snooze") {}.buttonStyle(.variant(.secondary))
        ButtonGroupSeparator()
        ButtonGroupMailMenu(variant: .secondary)
      }
    }
  }
}

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
  let variant: VariantButtonStyle.Variant
  var size: VariantButtonStyle.Size = .icon

  var body: some View {
    Button {} label: {
      ButtonGroupIcon(icon: icon, size: size)
    }
    .buttonStyle(.variant(variant, size: size))
    .accessibilityLabel(Text(label))
  }
}

struct ButtonGroupMailMenu: View {
  let variant: VariantButtonStyle.Variant

  var body: some View {
    DropdownMenu(align: .end) {
      DropdownMenuGroup {
        DropdownMenuItem("Mark as read", icon: Ph.listPlus.regular) {}
        DropdownMenuItem("Archive", icon: Ph.archive.regular) {}
      }
      DropdownMenuSeparator()
      DropdownMenuGroup {
        DropdownMenuItem("Snooze", icon: Ph.clock.regular) {}
        DropdownMenuItem("Add to calendar", icon: Ph.calendarPlus.regular) {}
        DropdownMenuItem("Label as", icon: Ph.tag.regular) {}
      }
      DropdownMenuSeparator()
      DropdownMenuGroup {
        DropdownMenuItem("Trash", icon: Ph.trash.regular, variant: .destructive) {}
      }
    } label: {
      ButtonGroupIcon(icon: Ph.dotsThree.bold, size: .icon)
    }
    .buttonStyle(.variant(variant, size: .icon))
    .accessibilityLabel(Text("More options"))
  }
}
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
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.DropdownMenu
import com.esuiss.ui.components.DropdownMenuGroup
import com.esuiss.ui.components.DropdownMenuItem
import com.esuiss.ui.components.DropdownMenuSeparator
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.MenuItemVariant
import com.esuiss.ui.components.PopupAlign

@Composable
fun ButtonGroupExample() {
    ButtonGroupMail(ButtonVariant.Secondary, separators = true)
}

@Composable
fun ButtonGroupIconButton(
    label: String,
    icon: androidx.compose.ui.graphics.vector.ImageVector,
    variant: ButtonVariant = ButtonVariant.Default,
    size: ButtonSize = ButtonSize.Icon,
    onClick: () -> Unit = {},
) {
    Button(onClick = onClick, variant = variant, size = size) {
        Icon(icon, contentDescription = label)
    }
}

@Composable
fun ButtonGroupMailMenu(variant: ButtonVariant) {
    var open by rememberSaveable { mutableStateOf(false) }
    DropdownMenu(
        expanded = open,
        onExpandedChange = { open = it },
        trigger = { ButtonGroupIconButton("More options", PhosphorIcons.Regular.DotsThree, variant) { open = true } },
        align = PopupAlign.End,
    ) {
        DropdownMenuGroup {
            DropdownMenuItem("Mark as read", onClick = {}, icon = PhosphorIcons.Regular.ListPlus)
            DropdownMenuItem("Archive", onClick = {}, icon = PhosphorIcons.Regular.Archive)
        }
        DropdownMenuSeparator()
        DropdownMenuGroup {
            DropdownMenuItem("Snooze", onClick = {}, icon = PhosphorIcons.Regular.Clock)
            DropdownMenuItem("Add to calendar", onClick = {}, icon = PhosphorIcons.Regular.CalendarPlus)
            DropdownMenuItem("Label as", onClick = {}, icon = PhosphorIcons.Regular.Tag)
        }
        DropdownMenuSeparator()
        DropdownMenuGroup {
            DropdownMenuItem("Trash", onClick = {}, icon = PhosphorIcons.Regular.Trash, variant = MenuItemVariant.Destructive)
        }
    }
}

@Composable
fun ButtonGroupMail(variant: ButtonVariant, separators: Boolean) {
    ButtonGroup {
        ButtonGroup {
            ButtonGroupIconButton("Go back", PhosphorIcons.Regular.ArrowLeft, variant)
        }
        ButtonGroup {
            Button("Archive", onClick = {}, variant = variant)
            if (separators) ButtonGroupSeparator()
            Button("Report", onClick = {}, variant = variant)
        }
        ButtonGroup {
            Button("Snooze", onClick = {}, variant = variant)
            if (separators) ButtonGroupSeparator()
            ButtonGroupMailMenu(variant)
        }
    }
}
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,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"

export default function ButtonGroupDemo() {
  return (
    <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>
  )
}
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(
  "flex w-fit items-stretch *:data-slot:bg-clip-border [&>[data-variant=ghost]]:rounded-full! [&>[data-variant=plain]]:rounded-full! *:focus-visible:relative *:focus-visible:z-10 has-[>[data-slot=button-group]]:gap-2 has-[>[data-variant=outline]]:*:data-[slot=input-group]:border-border has-[>[data-variant=outline]]:*:data-[slot=select-trigger]:border-border [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 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",
      },
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,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"

export default function ButtonGroupOutline() {
  return (
    <ButtonGroup orientation="horizontal">
      <ButtonGroup>
        <Button variant="outline" size="icon" aria-label="Go back">
          <ArrowLeftIcon />
        </Button>
      </ButtonGroup>
      <ButtonGroup>
        <Button variant="outline">Archive</Button>
        <Button variant="outline">Report</Button>
      </ButtonGroup>
      <ButtonGroup>
        <Button variant="outline">Snooze</Button>
        <DropdownMenu>
          <DropdownMenuTrigger
            render={
              <Button variant="outline" 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>
  )
}
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>
  )
}
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
        </Button>
        <Button variant="outline" size="icon-sm" aria-label="Add">
          <PlusIcon />
        </Button>
      </ButtonGroup>
      <ButtonGroup orientation="horizontal">
        <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 orientation="horizontal">
        <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>
  )
}
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
        </Button>
        <Button variant="outline" size="sm">
          4
        </Button>
        <Button variant="outline" size="sm">
          5
        </Button>
      </ButtonGroup>
      <ButtonGroup>
        <Button variant="outline" size="icon-sm" aria-label="Previous">
          <ArrowLeftIcon />
        </Button>
        <Button variant="outline" size="icon-sm" aria-label="Next">
          <ArrowRightIcon />
        </Button>
      </ButtonGroup>
    </ButtonGroup>
  )
}
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>
    </ButtonGroup>
  )
}
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">
        <ShareIcon data-icon="inline-start" />
        Share
      </Button>
    </ButtonGroup>
  )
}
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>
  )
}
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>
  )
}
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() {
  return (
    <ButtonGroup orientation="horizontal" className="w-96">
      <ButtonGroup>
        <Button variant="outline" size="icon-lg" aria-label="Add">
          <PlusIcon />
        </Button>
      </ButtonGroup>
      <ButtonGroup className="flex-1">
        <InputGroup>
          <InputGroupInput
            aria-label="Message"
            placeholder="Send a message..."
          />
          <InputGroupAddon align="inline-end">
            <Tooltip>
              <TooltipTrigger
                render={
                  <InputGroupButton size="icon-xs" aria-label="Voice mode" />
                }
              >
                <MicrophoneIcon />
              </TooltipTrigger>
              <TooltipContent>Voice mode</TooltipContent>
            </Tooltip>
          </InputGroupAddon>
        </InputGroup>
      </ButtonGroup>
    </ButtonGroup>
  )
}
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>
    </ButtonGroup>
  )
}
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,
} from "@/components/ui/dropdown-menu"

export default function ButtonGroupWithDropdownMenu() {
  return (
    <ButtonGroup orientation="horizontal">
      <Button variant="outline">Follow</Button>
      <DropdownMenu>
        <DropdownMenuTrigger
          render={
            <Button variant="outline" size="icon" aria-label="More options" />
          }
        >
          <CaretDownIcon />
        </DropdownMenuTrigger>
        <DropdownMenuContent align="end" className="w-56">
          <DropdownMenuGroup>
            <DropdownMenuItem>
              <WarningCircleIcon />
              Mute conversation
            </DropdownMenuItem>
            <DropdownMenuItem>
              <CopyIcon />
              Copy link
            </DropdownMenuItem>
            <DropdownMenuItem>
              <ShareIcon />
              Share conversation
            </DropdownMenuItem>
          </DropdownMenuGroup>
          <DropdownMenuSeparator />
          <DropdownMenuGroup>
            <DropdownMenuItem variant="destructive">
              <UserCircleMinusIcon />
              Unfollow
            </DropdownMenuItem>
          </DropdownMenuGroup>
        </DropdownMenuContent>
      </DropdownMenu>
    </ButtonGroup>
  )
}
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: "€" },
  { value: "gbp", label: "£" },
  { value: "try", label: "₺" },
]

export default function ButtonGroupWithSelect() {
  return (
    <ButtonGroup orientation="horizontal">
      <ButtonGroup>
        <Select defaultValue="usd" items={currencies}>
          <SelectTrigger aria-label="Currency" className="font-mono">
            <SelectValue />
          </SelectTrigger>
          <SelectContent>
            <SelectGroup>
              {currencies.map((currency) => (
                <SelectItem key={currency.value} value={currency.value}>
                  {currency.label}
                </SelectItem>
              ))}
            </SelectGroup>
          </SelectContent>
        </Select>
        <Input aria-label="Amount" placeholder="10.00" inputMode="decimal" />
      </ButtonGroup>
      <ButtonGroup>
        <Button variant="outline" size="icon-lg" aria-label="Send">
          <ArrowRightIcon />
        </Button>
      </ButtonGroup>
    </ButtonGroup>
  )
}
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">
      <Button variant="outline">
        <SparkleIcon data-icon="inline-start" />
        Assistant
      </Button>
      <Popover>
        <PopoverTrigger
          render={
            <Button variant="outline" size="icon" aria-label="Open popover" />
          }
        >
          <CaretDownIcon />
        </PopoverTrigger>
        <PopoverContent align="end" className="w-80">
          <PopoverHeader>
            <PopoverTitle>Start a new task</PopoverTitle>
            <PopoverDescription>
              Describe your task in natural language.
            </PopoverDescription>
          </PopoverHeader>
          <Textarea
            aria-label="Task description"
            placeholder="I need to..."
            className="resize-none"
          />
        </PopoverContent>
      </Popover>
    </ButtonGroup>
  )
}
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">
        <CaretRightIcon />
      </Button>
    </ButtonGroup>
  )
}
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"
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"
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"