Actions
Button Group
Joins a row or column of buttons and inputs into one connected, segmented control by collapsing shared borders and squaring inner radii.
actiongrouptoolbarsegmentedlayout
Horizontal
The default — buttons connect into a single segmented control.
Vertical
Stack the same group with `orientation="vertical"`.
Icon controls
Square icon buttons make a tidy pager / stepper.
With text addon
Pair buttons with a flush, non-interactive label via ButtonGroupText.
Active selection
Promote the chosen segment with the primary intent.
Source
Copy this into your project. Resolve its dependencies from the registryDependencies in the component's API entry.
"use client"
import { tv, type VariantProps } from "tailwind-variants"
import { cn } from "@/lib/utils"
/**
* ButtonGroup — quebi design system
*
* Joins a row (or column) of buttons / inputs into a single connected unit:
* shared borders are collapsed and the inner radii squared off so the children
* read as one segmented control. Self-contained — drop any buttons inside.
*
* Use `ButtonGroupText` for non-interactive labels or addons that sit flush
* with the buttons (e.g. units, prefixes).
*/
const buttonGroupStyles = tv({
base: [
"flex w-fit items-stretch",
// keep focused child above its neighbours so the ring isn't clipped
"*:focus-visible:relative *:focus-visible:z-10",
// nested groups get breathing room
"has-[>[data-slot=button-group]]:gap-2",
// text inputs flex to fill
"[&>input]:flex-1",
],
variants: {
orientation: {
horizontal:
"[&>*:not(:first-child)]:rounded-s-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-e-none",
vertical:
"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
},
},
defaultVariants: {
orientation: "horizontal",
},
})
export interface ButtonGroupProps
extends React.ComponentProps<"div">,
VariantProps<typeof buttonGroupStyles> {}
export function ButtonGroup({ className, orientation, ...props }: ButtonGroupProps) {
return (
// biome-ignore lint/a11y/useSemanticElements: no semantic HTML element for button group role
<div
role="group"
data-slot="button-group"
data-orientation={orientation}
className={cn(buttonGroupStyles({ orientation }), className)}
{...props}
/>
)
}
export function ButtonGroupText({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="button-group-text"
className={cn(
"flex items-center gap-2 whitespace-nowrap",
"rounded-quebi-sm border border-cyan-500/10 bg-white/[0.03] px-4",
"font-sans text-sm font-medium text-quebi-fg-muted",
"*:data-[slot=icon]:pointer-events-none *:data-[slot=icon]:shrink-0",
"[&_[data-slot=icon]:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
)
}