ButtonGroup
A set of buttons visually joined together, used for segmented actions or view toggles. Supports horizontal and vertical orientations with shared variant and size propagation.
AccessibleDark Mode2 Variants3 Sizes2 Orientations
Install
$
npx react-principles add button-group01
Theme Preview
Both variants across light and dark themes — forced styling for accurate side-by-side comparison.
Light
LeftCenterRight
SaveCancel
Dark
LeftCenterRight
SaveCancel
02
Live Demo
Variant
Size
Orientation
03
Code Snippet
src/ui/ButtonGroup.tsx
import { ButtonGroup } from "@/ui/ButtonGroup"; import { Button } from "@/ui/Button"; // Horizontal (default) <ButtonGroup variant="default"> <Button>Left</Button> <Button>Center</Button> <Button>Right</Button> </ButtonGroup> // Outline variant <ButtonGroup variant="outline"> <Button>Save</Button> <Button>Cancel</Button> </ButtonGroup> // Vertical orientation <ButtonGroup orientation="vertical"> <Button>Top</Button> <Button>Middle</Button> <Button>Bottom</Button> </ButtonGroup> // Sizes <ButtonGroup size="sm">...</ButtonGroup> <ButtonGroup size="md">...</ButtonGroup> <ButtonGroup size="lg">...</ButtonGroup>
04
Copy-Paste (Single File)
ButtonGroup.tsx
import { Children, cloneElement, isValidElement, type HTMLAttributes, type ReactElement, type ReactNode, } from "react"; import { cn } from "@/lib/utils"; import type { ButtonProps } from "./Button"; export type ButtonGroupOrientation = "horizontal" | "vertical"; export type ButtonGroupVariant = "default" | "outline"; export type ButtonGroupSize = "sm" | "md" | "lg"; export interface ButtonGroupProps extends HTMLAttributes<HTMLDivElement> { orientation?: ButtonGroupOrientation; variant?: ButtonGroupVariant; size?: ButtonGroupSize; disabled?: boolean; children: ReactNode; } const VARIANT_MAP: Record<ButtonGroupVariant, ButtonProps["variant"]> = { default: "primary", outline: "outline", }; function getGroupedRadiusClass( index: number, total: number, isVertical: boolean, ): string { if (total <= 1) return ""; const isFirst = index === 0; const isLast = index === total - 1; if (isVertical) { if (isFirst) return "rounded-b-none"; if (isLast) return "rounded-t-none"; return "rounded-none"; } if (isFirst) return "rounded-r-none"; if (isLast) return "rounded-l-none"; return "rounded-none"; } export function ButtonGroup({ orientation = "horizontal", variant = "default", size = "md", disabled = false, children, className, ...props }: ButtonGroupProps) { const isVertical = orientation === "vertical"; const mappedVariant = VARIANT_MAP[variant]; const count = Children.count(children); return ( <div role="group" className={cn( isVertical ? "inline-flex flex-col" : "inline-flex items-center", className, )} {...props} > {Children.map(children, (child, index) => { if (!isValidElement(child)) return child; const childProps = child.props as Partial<ButtonProps>; const radiusClass = getGroupedRadiusClass(index, count, isVertical); const spacingClass = isVertical ? "not-first:-mt-px" : "not-first:-ml-px"; return cloneElement(child as ReactElement<ButtonProps>, { variant: childProps.variant ?? mappedVariant, size: childProps.size ?? size, disabled: childProps.disabled ?? disabled, className: cn(radiusClass, spacingClass, childProps.className), }); })} </div> ); }
05
Props
Extends all native HTMLDivElement attributes (id, aria-label, etc.).
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | "horizontal" | "vertical" | "horizontal" | Layout direction of the button group. |
variant | "default" | "outline" | "default" | Variant applied to all child Buttons. "default" maps to primary, "outline" maps to outline. |
size | "sm" | "md" | "lg" | "md" | Size applied to all child Buttons. |
disabled | boolean | false | Disables all child Buttons. |
children | ReactNode | — | Must contain <Button> elements. |
className | string | — | Extra CSS classes on the container div. |