Groups related buttons into a single attached control, with optional separators.
Installation
pnpm dlx shadcn@latest add https://rdf.dev.360online.com/r/button-group.jsonInstalls to @/components/ui/button-group.
Manual installation
Install the dependencies:
pnpm add @base-ui/react@^1.8.0 class-variance-authority@^0.7.1Add the components it builds on: separator.
Copy the source into your project:
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'
// A filled button paints inside its transparent border (bg-clip-padding), which leaves it a pixel short beside outlined
// buttons in a group; bg-clip-border lets it fill to the group's edge
const buttonGroupVariants = cva(
"group/button-group flex w-fit items-stretch [&>[data-variant]]:bg-clip-border *:focus-visible:relative *:focus-visible:z-10 has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-lg [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
{
variants: {
orientation: {
horizontal:
'*:data-slot:rounded-r-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-lg! [&>[data-slot]~[data-slot]]:rounded-l-none [&>[data-slot]~[data-slot]]:border-l-0',
vertical:
'flex-col *:data-slot:rounded-b-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-lg! [&>[data-slot]~[data-slot]]:rounded-t-none [&>[data-slot]~[data-slot]]:border-t-0',
},
},
defaultVariants: {
orientation: 'horizontal',
},
},
)
function ButtonGroup({
className,
orientation,
...props
}: React.ComponentProps<'div'> & VariantProps<typeof buttonGroupVariants>) {
return (
<div
role="group"
data-slot="button-group"
data-orientation={orientation}
className={cn(buttonGroupVariants({ orientation }), className)}
{...props}
/>
)
}
function ButtonGroupText({ className, render, ...props }: useRender.ComponentProps<'div'>) {
return useRender({
defaultTagName: 'div',
props: mergeProps<'div'>(
{
className: cn(
"flex items-center gap-2 rounded-lg border bg-muted px-2.5 text-sm font-medium [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className,
),
},
props,
),
render,
state: {
slot: 'button-group-text',
},
})
}
function ButtonGroupSeparator({
className,
orientation = 'vertical',
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="button-group-separator"
orientation={orientation}
className={cn(
'relative self-stretch bg-input data-horizontal:mx-px data-horizontal:w-auto data-vertical:my-px data-vertical:h-auto',
className,
)}
{...props}
/>
)
}
export { ButtonGroup, ButtonGroupSeparator, ButtonGroupText, buttonGroupVariants }Groups related buttons — or an input together with buttons — into one visual control. Adjacent borders and corner radii are merged, and nested groups are spaced apart.
Features
- Horizontal and vertical orientation
ButtonGroupSeparatorfor split buttonsButtonGroupTextfor static prefixes/suffixesButtonGroupTextsupports therenderprop (e.g. to render aLabel)- Nested
ButtonGroups are spaced apart automatically - Works with any
Buttonvariant and size (includingxsand icon sizes),Input, orSelect buttonGroupVariantsis exported for reuse
Usage
import { ButtonGroup, ButtonGroupSeparator, ButtonGroupText } from "@/components/ui/button-group"
import { Button } from "@/components/ui/button"
import { ChevronDown } from "lucide-react"
<ButtonGroup aria-label="Message actions">
<Button variant="outline">Reply</Button>
<Button variant="outline">Forward</Button>
<Button variant="outline">Archive</Button>
</ButtonGroup>
// Split button
<ButtonGroup>
<Button>Send decision</Button>
<ButtonGroupSeparator />
<Button size="icon" aria-label="More ways to send">
<ChevronDown />
</Button>
</ButtonGroup>Examples
Default
Actions on a message in the case inbox. Give the group an aria-label when its purpose isn’t obvious from the buttons.
Sizes
Buttons keep their own size inside a group; use matching text and icon sizes.
Vertical
orientation="vertical" stacks the buttons, here as zoom controls on a property map.
With separator
A split button: the main action plus a dropdown with alternatives, divided by ButtonGroupSeparator.
With text
ButtonGroupText with render={<Label />} renders a Label as a prefix for an input.
Stepper
A number stepper built from two icon buttons and ButtonGroupText. The buttons use focusableWhenDisabled, so keyboard
focus stays on a button when it reaches the limit and becomes disabled.
Nested groups
Nested groups are spaced apart automatically, like the week navigation and Today button in a calendar header.
Disabled
Disable a single button when that action isn't available.
Props
ButtonGroup-orientation?: 'horizontal' | 'vertical'(default'horizontal'), rendersrole="group"ButtonGroupText-render?: React.ReactElement, the element to render instead of adivButtonGroupSeparator- props ofSeparator;orientationdefaults to'vertical'
Related Components
- Button
- Toggle Group
- Input Group