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:
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.
The container that joins its children and sets the orientation.
Prop
Type
Default
Description
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.
Add the pieces it builds on: Button, Icon, Separator, Text, and the primitives components/primitives/types.tsx, components/primitives/use-render.tsx.
Copy the source into components/ui/button-group.tsx:
Update the import paths to match your project.
Install the dependencies:
npx expo install class-variance-authority cn
Add the pieces it builds on: Button, Icon, Separator, Text, and the primitives components/primitives/types.tsx, components/primitives/use-render.tsx.
Copy the source into components/ui/button-group.tsx:
Update the import paths to match your project.
Add the pieces it builds on: Button, Icon, Separator, Text, and the primitives components/primitives/types.tsx, components/primitives/use-render.tsx.
Copy the source into components/ui/button-group.tsx:
Update the import paths to match your project.
Target iOS 17 or later.
Copy the theme files every view uses:
swift/Theme/Theme.swift
import SwiftUIimport UIKitstruct 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 SwiftUIpublic 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))
Copy the source and every file it uses:
swift/Components/ButtonGroup.swift
import SwiftUIextension 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 SwiftUIpublic 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 SwiftUIpublic 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 } }
Add the files to your app target.
Set minSdk to 24 or higher and add the dependencies:
package com.esuiss.ui.themeimport androidx.compose.runtime.Immutableimport androidx.compose.ui.graphics.Colorimport androidx.compose.ui.unit.dp@Immutabledata 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,
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.
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"