Join adjacent actions into one compact control surface while retaining each button's visible keyboard focus and accessible name.
Q3 product brief
Last edited a few minutes ago
Ready to share
"use client";
import * as React from "react";
import { Copy, Download, Share2 } from "lucide-react";
import {
ButtonGroup,
ButtonGroupText,
} from "@/components/ballmac/button-group";
const actionClass =
"inline-flex h-9 items-center justify-center gap-2 border bg-background px-3 text-sm font-medium outline-none hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/50 [&_svg]:size-4";
export default function ButtonGroupDemo() {
const [message, setMessage] = React.useState("Ready to share");
return (
<div className="w-full max-w-sm rounded-xl border bg-card p-5 shadow-sm">
<p className="mb-1 text-sm font-semibold">Q3 product brief</p>
<p className="mb-4 text-xs text-muted-foreground">
Last edited a few minutes ago
</p>
<ButtonGroup aria-label="Document actions" className="max-w-full">
<ButtonGroupText>Share</ButtonGroupText>
<button
type="button"
className={actionClass}
onClick={() => setMessage("Link copied")}
>
<Copy aria-hidden="true" />
<span className="hidden sm:inline">Copy link</span>
<span className="sr-only sm:hidden">Copy link</span>
</button>
<button
type="button"
aria-label="Download"
className={actionClass}
onClick={() => setMessage("Download prepared")}
>
<Download aria-hidden="true" />
</button>
<button
type="button"
aria-label="Share"
className={actionClass}
onClick={() => setMessage("Share panel ready")}
>
<Share2 aria-hidden="true" />
</button>
</ButtonGroup>
<p role="status" className="mt-3 text-xs text-muted-foreground">
{message}
</p>
</div>
);
}Installation
$ pnpm dlx shadcn@latest add @ballmac/button-groupCopy the source into your project.
components/ballmac/button-group.tsx// Ballmac UI: Button Group. https://ui.ballmac.com/components/button-group import * as React from "react"; import { cn } from "@/lib/utils"; type ButtonGroupProps = React.ComponentProps<"div"> & { /** Direction of adjacent controls. */ orientation?: "horizontal" | "vertical"; }; function ButtonGroup({ className, orientation = "horizontal", ...props }: ButtonGroupProps) { return ( <div data-slot="button-group" data-orientation={orientation} role="group" className={cn( "group/button-group inline-flex max-w-full items-stretch rounded-md shadow-xs [&>*]:relative [&>*]:min-w-0 [&>*]:shadow-none [&>*:focus-visible]:z-10", orientation === "horizontal" ? "flex-row [&>*:not(:first-child)]:-ms-px [&>*:not(:first-child)]:rounded-s-none [&>*:not(:last-child)]:rounded-e-none" : "flex-col [&>*:not(:first-child)]:-mt-px [&>*:not(:first-child)]:rounded-t-none [&>*:not(:last-child)]:rounded-b-none", className, )} {...props} /> ); } type ButtonGroupTextProps = React.ComponentProps<"span">; function ButtonGroupText({ className, ...props }: ButtonGroupTextProps) { return ( <span data-slot="button-group-text" className={cn( "inline-flex min-h-9 items-center border bg-muted/50 px-3 text-sm text-muted-foreground", className, )} {...props} /> ); } type ButtonGroupSeparatorProps = React.ComponentProps<"span">; function ButtonGroupSeparator({ className, ...props }: ButtonGroupSeparatorProps) { return ( <span data-slot="button-group-separator" aria-hidden="true" className={cn("z-10 w-px self-stretch bg-border group-data-[orientation=vertical]/button-group:h-px group-data-[orientation=vertical]/button-group:w-full", className)} {...props} /> ); } export { ButtonGroup, ButtonGroupText, ButtonGroupSeparator, type ButtonGroupProps, type ButtonGroupTextProps, type ButtonGroupSeparatorProps, };Update the import paths to match your project setup.
Usage
import { ButtonGroup, ButtonGroupText, ButtonGroupSeparator } from "@/components/ballmac/button-group"The full example is in the Code tab above.
Examples
Orientations
Vertical groups keep related tools compact in narrow panels.
import { Plus, Minus } from "lucide-react";
import {
ButtonGroup,
ButtonGroupText,
} from "@/components/ballmac/button-group";
export default function ButtonGroupStates() {
return (
<div className="flex w-full max-w-xs items-start gap-4">
<ButtonGroup orientation="vertical" aria-label="Zoom controls">
<button
type="button"
aria-label="Zoom in"
className="flex size-9 items-center justify-center border bg-background outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
>
<Plus aria-hidden="true" className="size-4" />
</button>
<ButtonGroupText>100%</ButtonGroupText>
<button
type="button"
aria-label="Zoom out"
className="flex size-9 items-center justify-center border bg-background outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
>
<Minus aria-hidden="true" className="size-4" />
</button>
</ButtonGroup>
<p className="text-sm text-muted-foreground">
Vertical groups keep related tools compact in narrow panels.
</p>
</div>
);
}API reference
| Prop | Type | Default |
|---|---|---|
orientationDirection of adjacent controls. | "horizontal" | "vertical" | "horizontal" |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Tab | Moves between each button in document order |
| Enter / Space | Activates the focused button |
Use with AI
Groups closely related actions without turning them into a selection widget. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Button Group (@ballmac/button-group) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Adjacent document or editor actions
- Compact controls with one shared boundary
Not for
- Mutually exclusive options; use toggle-group
- Unrelated actions
Registry JSON: https://ui.ballmac.com/r/button-group.json
Credits
Free to use in personal and commercial projects.
- npm
- None
- Registry
- shadcn/utils
Pairs well with
Button
A button with six variants, three sizes, a pill shape and a built-in loading state. buttonVariants() styles links the same way.
Accordion
Vertically stacked disclosure sections on Radix Accordion, single or multiple open, with hairline dividers, a rotating plus and height animation.
Avatar
A Radix avatar with image and initials fallback, three sizes, an optional presence dot, and AvatarGroup for overlapping stacks with a +N overflow counter.
Badge
A small pill label in four variants with an optional status dot and success, warning and error tones. Style links with badgeVariants().