Primitives
Button group
Joined button group for toolbars, split actions, and segmented controls.
Composition
Joined actions
- <ButtonGroup>
- <ButtonGroupText />
- primary action
- <Button />
- <ButtonGroupSeparator />
- secondary action
- <Button />
Installation
First install and activate one skin. Core deliberately contains no visual token defaults.
The Control UI source installs this primitive from src/registry/sources/control-ui/ui/button-group.tsx.
npx shadcn@latest add https://control-ui.dev/r/button-group.jsonDependencies
Raw code
Primary installed source
"use client";import type { ComponentProps, CSSProperties } from "react";import type { HoverIndicator } from "@/components/control-ui/control-props";import type { ControlSize } from "@/components/control-ui/control-variants";import { TrackHighlight } from "@/components/control-ui/extensions/track-highlight";import type { ButtonGroupKnobStyle } from "@/components/control-ui/knob-contracts/button-group-knobs";import { cn } from "@/components/control-ui/lib/cn";import { useSkin } from "@/components/control-ui/skin-provider";import { ButtonTrackContext, buttonTrackStructureClasses } from "@/components/control-ui/ui/button";export type ButtonGroupTextProps = Omit< ComponentProps<"div"> & { size?: ControlSize; }, "style"> & { style?: CSSProperties & ButtonGroupKnobStyle };export type ButtonGroupProps = ComponentProps<"div"> & { orientation?: "horizontal" | "vertical"; indicator?: HoverIndicator;} & { style?: CSSProperties & ButtonGroupKnobStyle };export type ButtonGroupSeparatorProps = Omit< ComponentProps<"div"> & { orientation?: "horizontal" | "vertical"; }, "style"> & { style?: CSSProperties & ButtonGroupKnobStyle };export function ButtonGroup({ orientation = "horizontal", indicator, className, children, ...props }: ButtonGroupProps) { const skin = useSkin(); const resolvedIndicator = indicator ?? skin.indicators?.["button-group"] ?? "none"; const tracksHover = resolvedIndicator === "hover"; return ( <ButtonTrackContext value={tracksHover}> {/* biome-ignore lint/a11y/useSemanticElements: a segmented control is a labelled group, not a fieldset form group. */} <div role="group" data-control-ui="button-group" data-control-family="button-group" data-slot="root" data-orientation={orientation} data-track={resolvedIndicator} className={cn(buttonTrackStructureClasses, "inline-flex w-fit items-stretch data-[orientation=vertical]:flex-col", className)} {...props} > {children} {tracksHover ? <TrackHighlight className="z-0" /> : null} </div> </ButtonTrackContext> );}export function ButtonGroupText({ size = "sm", className, ...props }: ButtonGroupTextProps) { return ( <div data-control-ui="button-group" data-control-family="button-group" data-slot="text" data-size={size} className={cn("inline-flex items-center whitespace-nowrap [&>svg]:pointer-events-none [&>svg]:size-4 [&>svg]:shrink-0", className)} {...props} /> );}export function ButtonGroupSeparator({ orientation = "vertical", className, ...props }: ButtonGroupSeparatorProps) { return ( <div aria-hidden="true" data-control-ui="button-group" data-control-family="button-group" data-slot="separator" data-orientation={orientation} className={cn("shrink-0 self-stretch", orientation === "vertical" ? "w-px" : "h-px w-full", className)} {...props} /> );}Knobs
Typed custom properties the recipe paints with. Set one on the root — style, a utility class, or a skin — and every slot inherits it.
--cui-button-group-* · 4 knobsHow the cascade resolves--cui-button-group-text-radius<length-percentage>var(--radius-control)--cui-button-group-text-background<color>var(--control-fill)--cui-button-group-text-foreground<color>var(--muted-foreground)--cui-button-group-separator-background<color>var(--border)