implement
Button Group

Button Group

Buttons joined into one control.

https://
import { Div } from "@implementjs/core";
import { BoldIcon, ItalicIcon, UnderlineIcon } from "@implementjs/lucide";
import { Button } from "@/lib/components/ui/button";
import {
	ButtonGroup,
	ButtonGroupSeparator,
	ButtonGroupText,
} from "@/lib/components/ui/button-group";
import { Input } from "@/lib/components/ui/input";

export default function ButtonGroupDemo() {
	return Div(
		{ class: "flex flex-col items-center gap-4" },
		ButtonGroup(
			Button({ variant: "outline", size: "icon-sm", "aria-label": "Bold" }, BoldIcon()),
			Button({ variant: "outline", size: "icon-sm", "aria-label": "Italic" }, ItalicIcon()),
			Button({ variant: "outline", size: "icon-sm", "aria-label": "Underline" }, UnderlineIcon()),
		),
		ButtonGroup(
			Button({ variant: "outline", size: "sm" }, "Merge"),
			ButtonGroupSeparator(),
			Button({ variant: "outline", size: "sm" }, "Squash"),
			Button({ variant: "outline", size: "sm" }, "Rebase"),
		),
		ButtonGroup(
			{ class: "w-full max-w-sm" },
			ButtonGroupText("https://"),
			Input({ placeholder: "example.com", "aria-label": "Site" }),
			Button({ variant: "outline" }, "Add"),
		),
	);
}

Installation

npx jsrepo add @implementjs/ui/button-group

jsrepo pulls separator along with it, and installs tailwind-variants.

Usage

import { ButtonGroup } from "@/lib/components/ui/button-group";

ButtonGroup(
	Button({ variant: "outline", size: "sm" }, "Merge"),
	Button({ variant: "outline", size: "sm" }, "Squash"),
	Button({ variant: "outline", size: "sm" }, "Rebase"),
);

The group squares off the inner corners and drops the doubled borders between children, so a row of outline buttons reads as one segmented thing rather than as three.

It joins by position, not by type

The rules are written against :first-child and :last-child, so anything in the row joins on the same terms. A select trigger, an input, and a ButtonGroupText prefix all fit:

ButtonGroup(
	ButtonGroupText("https://"),
	Input({ placeholder: "example.com", "aria-label": "Site" }),
	Button({ variant: "outline" }, "Add"),
);

Separators

Because the group removes the borders between children, a visible divider has to go back in on purpose:

ButtonGroup(Button("Merge"), ButtonGroupSeparator(), Button("Squash"));

Vertical

orientation: "vertical" stacks the row and joins the top and bottom edges instead of the left and right.

API Reference

ButtonGroup

Sets role="group". Squares off the inner corners and drops the doubled borders between children, so the row reads as one control. It styles by position rather than by type — a select trigger or an input joins the row on the same terms as a button. Renders a Div; extra props are forwarded onto it.

PropTypeDefaultDescription
orientation"horizontal" | "vertical""horizontal"Which edges are joined. Also set as data-orientation.

ButtonGroupText

A non-interactive label sitting in the row, styled like a button — a prefix, a unit, a count. Renders a Div; extra props are forwarded onto it.

ButtonGroupSeparator

A divider inside the group. The group removes the borders between children, so this is how a visible line goes back in. Renders a Separator; extra props are forwarded onto it.

PropTypeDefaultDescription
orientation"horizontal" | "vertical""vertical"Which way the line runs.