Zaidan

Command Palette

Search for a command to run...

GitHub

Button Group

A container that groups related buttons together with consistent styling.

Installation

CLI

Manual

Copy and paste the following code into your project.

import { cva, type VariantProps } from "class-variance-authority";
import { type ComponentProps, mergeProps, splitProps } from "solid-js";
import { cn } from "~/lib/utils";
import { Separator } from "~/components/ui/separator";
const buttonGroupVariants = cva(
"z-button-group flex w-fit items-stretch *:focus-visible:relative *:focus-visible:z-10 [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
{
variants: {
orientation: {
horizontal:
"z-button-group-orientation-horizontal *:data-slot:rounded-r-none [&>[data-slot]~[data-slot]]:rounded-l-none [&>[data-slot]~[data-slot]]:border-l-0",
vertical:
"z-button-group-orientation-vertical flex-col *:data-slot:rounded-b-none [&>[data-slot]~[data-slot]]:rounded-t-none [&>[data-slot]~[data-slot]]:border-t-0",
},
},
defaultVariants: {
orientation: "horizontal",
},
},
);
type ButtonGroupProps = ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>;
const ButtonGroup = (props: ButtonGroupProps) => {
const [local, others] = splitProps(props, ["class", "orientation"]);
return (
// biome-ignore lint/a11y/useSemanticElements: <exception for button group>
<div
class={cn(buttonGroupVariants({ orientation: local.orientation }), local.class)}
data-orientation={local.orientation}
data-slot="button-group"
role="group"
{...others}
/>
);
};
type ButtonGroupTextProps = ComponentProps<"div">;
const ButtonGroupText = (props: ButtonGroupTextProps) => {
const [local, others] = splitProps(props, ["class"]);
return (
<div
data-slot="button-group-text"
class={cn("z-button-group-text flex items-center [&_svg]:pointer-events-none", local.class)}
{...others}
/>
);
};
type ButtonGroupSeparatorProps = ComponentProps<typeof Separator>;
const ButtonGroupSeparator = (props: ButtonGroupSeparatorProps) => {
const mergedProps = mergeProps({ orientation: "vertical" } as const, props);
const [local, others] = splitProps(mergedProps, ["class", "orientation"]);
return (
<Separator
class={cn(
"relative z-button-group-separator self-stretch data-[orientation=horizontal]:mx-px data-[orientation=vertical]:my-px data-[orientation=vertical]:h-auto data-[orientation=horizontal]:w-auto",
local.class,
)}
data-slot="button-group-separator"
orientation={local.orientation}
{...others}
/>
);
};
export { ButtonGroup, ButtonGroupText, ButtonGroupSeparator, buttonGroupVariants };

Accessibility

  • The ButtonGroup component has the role attribute set to group.
  • Use Tab to navigate between the buttons in the group.
  • Use aria-label or aria-labelledby to label the button group.
<ButtonGroup aria-label="Button group">
<Button>Button 1</Button>
<Button>Button 2</Button>
</ButtonGroup>

ButtonGroup vs ToggleGroup

  • Use the ButtonGroup component when you want to group buttons that perform an action.
  • Use the ToggleGroup component when you want to group buttons that toggle a state.

Kobalte particularity

When using Select inside a ButtonGroup, you need to handle yourself the rounded border or not. It is necessary because Kobalte wrap Select inside a div, and button group expect a button obviously

Examples

Here are the source code of all the examples from the preview page:

Basic

import { Button } from "~/components/ui/button";
import { ButtonGroup } from "~/components/ui/button-group";
function ButtonGroupBasic() {
return (
<Example title="Basic">
<div class="flex flex-col gap-4">
<ButtonGroup>
<Button variant="outline">Button</Button>
<Button variant="outline">Another Button</Button>
</ButtonGroup>
</div>
</Example>
);
}

With Input

import { Button } from "~/components/ui/button";
import { ButtonGroup } from "~/components/ui/button-group";
import { Input } from "~/components/ui/input";
function ButtonGroupWithInput() {
return (
<Example title="With Input">
<div class="flex flex-col gap-4">
<ButtonGroup>
<Button variant="outline">Button</Button>
<Input placeholder="Type something here..." />
</ButtonGroup>
<ButtonGroup>
<Input placeholder="Type something here..." />
<Button variant="outline">Button</Button>
</ButtonGroup>
</div>
</Example>
);
}

With Text

import { Button } from "~/components/ui/button";
import { ButtonGroup, ButtonGroupText } from "~/components/ui/button-group";
import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
function ButtonGroupWithText() {
return (
<Example title="With Text">
<div class="flex flex-col gap-4">
<ButtonGroup>
<ButtonGroupText>Text</ButtonGroupText>
<Button variant="outline">Another Button</Button>
</ButtonGroup>
<ButtonGroup>
<ButtonGroupText>
<Label for="input-text">GPU Size</Label>
</ButtonGroupText>
<Input id="input-text" placeholder="Type something here..." />
</ButtonGroup>
</div>
</Example>
);
}

With Dropdown

import {
AlertTriangle,
Check,
ChevronDown,
Copy,
Share,
Trash,
UserRoundX,
VolumeX,
} from "lucide-solid";
import { Button } from "~/components/ui/button";
import { ButtonGroup } from "~/components/ui/button-group";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "~/components/ui/dropdown-menu";
function ButtonGroupWithDropdown() {
return (
<Example title="With Dropdown">
<div class="flex flex-col gap-4">
<ButtonGroup>
<Button variant="outline">Update</Button>
<DropdownMenu>
<DropdownMenuTrigger as={Button} variant="outline" size="icon" class="">
<ChevronDown />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Disable</DropdownMenuItem>
<DropdownMenuItem variant="destructive">Uninstall</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline">Follow</Button>
<DropdownMenu>
<DropdownMenuTrigger as={Button} variant="outline" size="icon" class="">
<ChevronDown />
</DropdownMenuTrigger>
<DropdownMenuContent class="w-50">
<DropdownMenuGroup>
<DropdownMenuItem>
<VolumeX />
Mute Conversation
</DropdownMenuItem>
<DropdownMenuItem>
<Check />
Mark as Read
</DropdownMenuItem>
<DropdownMenuItem>
<AlertTriangle />
Report Conversation
</DropdownMenuItem>
<DropdownMenuItem>
<UserRoundX />
Block User
</DropdownMenuItem>
<DropdownMenuItem>
<Share />
Share Conversation
</DropdownMenuItem>
<DropdownMenuItem>
<Copy />
Copy Conversation
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem variant="destructive">
<Trash />
Delete Conversation
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</ButtonGroup>
</div>
</Example>
);
}

With Select

import { ArrowRight } from "lucide-solid";
import { Button } from "~/components/ui/button";
import { ButtonGroup } from "~/components/ui/button-group";
import { Field } from "~/components/ui/field";
import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "~/components/ui/select";
function ButtonGroupWithSelect() {
return (
<Example title="With Select">
<Field>
<Label for="amount">Amount</Label>
<ButtonGroup>
<Select
options={currencyItems}
optionValue="value"
optionTextValue="label"
defaultValue={currencyItems[0]}
itemComponent={(props) => (
<SelectItem item={props.item}>{props.item.rawValue.label}</SelectItem>
)}
>
<SelectTrigger class="rounded-r-none">
<SelectValue<(typeof currencyItems)[number]>>
{(state) => state.selectedOption()?.label}
</SelectValue>
</SelectTrigger>
<SelectContent />
</Select>
<Input placeholder="Enter amount to send" class="rounded-l-none border-l-0" />
<Button variant="outline">
<ArrowRight />
</Button>
</ButtonGroup>
</Field>
</Example>
);
}

With Icons

import { FlipHorizontal, FlipVertical, RotateCw } from "lucide-solid";
import { Button } from "~/components/ui/button";
import { ButtonGroup } from "~/components/ui/button-group";
function ButtonGroupWithIcons() {
return (
<Example title="With Icons">
<div class="flex flex-col gap-4">
<ButtonGroup>
<Button variant="outline">
<FlipHorizontal />
</Button>
<Button variant="outline">
<FlipVertical />
</Button>
<Button variant="outline">
<RotateCw />
</Button>
</ButtonGroup>
</div>
</Example>
);
}

With Input Group

import { Search } from "lucide-solid";
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "~/components/ui/input-group";
function ButtonGroupWithInputGroup() {
return (
<Example title="With Input Group">
<div class="flex flex-col gap-4">
<InputGroup>
<InputGroupInput placeholder="Type to search..." />
<InputGroupAddon align="inline-start" class="text-muted-foreground">
<Search />
</InputGroupAddon>
</InputGroup>
</div>
</Example>
);
}

With Fields

import { Minus, Plus } from "lucide-solid";
import { Button } from "~/components/ui/button";
import { ButtonGroup } from "~/components/ui/button-group";
import { Field, FieldGroup } from "~/components/ui/field";
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "~/components/ui/input-group";
import { Label } from "~/components/ui/label";
function ButtonGroupWithFields() {
return (
<Example title="With Fields">
<FieldGroup class="grid grid-cols-3 gap-4">
<Field class="col-span-2">
<Label for="width">Width</Label>
<ButtonGroup>
<InputGroup>
<InputGroupInput id="width" />
<InputGroupAddon class="text-muted-foreground">W</InputGroupAddon>
<InputGroupAddon align="inline-end" class="text-muted-foreground">
px
</InputGroupAddon>
</InputGroup>
<Button variant="outline" size="icon">
<Minus />
</Button>
<Button variant="outline" size="icon">
<Plus />
</Button>
</ButtonGroup>
</Field>
</FieldGroup>
</Example>
);
}

With Like

import { Heart } from "lucide-solid";
import { Button } from "~/components/ui/button";
import { ButtonGroup } from "~/components/ui/button-group";
function ButtonGroupWithLike() {
return (
<Example title="With Like">
<ButtonGroup>
<Button variant="outline">
<Heart data-icon="inline-start" /> Like
</Button>
<Button as="span" variant="outline" size="icon" class="w-12">
1.2K
</Button>
</ButtonGroup>
</Example>
);
}

With Select and Input

import { ButtonGroup } from "~/components/ui/button-group";
import { Input } from "~/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "~/components/ui/select";
function ButtonGroupWithSelectAndInput() {
return (
<Example title="With Select and Input">
<ButtonGroup>
<Select
options={durationItems}
optionValue="value"
optionTextValue="label"
defaultValue={durationItems[0]}
itemComponent={(props) => (
<SelectItem item={props.item}>{props.item.rawValue.label}</SelectItem>
)}
>
<SelectTrigger id="duration" class="rounded-r-none">
<SelectValue<(typeof durationItems)[number]>>
{(state) => state.selectedOption()?.label}
</SelectValue>
</SelectTrigger>
<SelectContent />
</Select>
<Input class="rounded-l-none border-l-0" />
</ButtonGroup>
</Example>
);
}

Nested

import { AudioLines, Plus } from "lucide-solid";
import { Button } from "~/components/ui/button";
import { ButtonGroup } from "~/components/ui/button-group";
import { InputGroup, InputGroupAddon, InputGroupInput } from "~/components/ui/input-group";
import { Tooltip, TooltipContent, TooltipTrigger } from "~/components/ui/tooltip";
function ButtonGroupNested() {
return (
<Example title="Nested">
<ButtonGroup>
<ButtonGroup>
<Button variant="outline" size="icon">
<Plus />
</Button>
</ButtonGroup>
<ButtonGroup>
<InputGroup>
<InputGroupInput placeholder="Send a message..." />
<Tooltip>
<TooltipTrigger as={InputGroupAddon} align="inline-end">
<AudioLines />
</TooltipTrigger>
<TooltipContent>Voice Mode</TooltipContent>
</Tooltip>
</InputGroup>
</ButtonGroup>
</ButtonGroup>
</Example>
);
}

Pagination

import { ArrowLeft, ArrowRight } from "lucide-solid";
import { Button } from "~/components/ui/button";
import { ButtonGroup } from "~/components/ui/button-group";
function ButtonGroupPagination() {
return (
<Example title="Pagination">
<ButtonGroup>
<Button variant="outline" size="sm">
<ArrowLeft data-icon="inline-start" />
Previous
</Button>
<Button variant="outline" size="sm">
1
</Button>
<Button variant="outline" size="sm">
2
</Button>
<Button variant="outline" size="sm">
3
</Button>
<Button variant="outline" size="sm">
4
</Button>
<Button variant="outline" size="sm">
5
</Button>
<Button variant="outline" size="sm">
Next
<ArrowRight data-icon="inline-end" />
</Button>
</ButtonGroup>
</Example>
);
}

Pagination Split

import { ArrowLeft, ArrowRight } from "lucide-solid";
import { Button } from "~/components/ui/button";
import { ButtonGroup } from "~/components/ui/button-group";
function ButtonGroupPaginationSplit() {
return (
<Example title="Pagination Split">
<ButtonGroup>
<ButtonGroup>
<Button variant="outline" size="sm">
1
</Button>
<Button variant="outline" size="sm">
2
</Button>
<Button variant="outline" size="sm">
3
</Button>
<Button variant="outline" size="sm">
4
</Button>
<Button variant="outline" size="sm">
5
</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline" size="icon-xs">
<ArrowLeft />
</Button>
<Button variant="outline" size="icon-xs">
<ArrowRight />
</Button>
</ButtonGroup>
</ButtonGroup>
</Example>
);
}
import { ArrowLeft, ArrowRight } from "lucide-solid";
import { Button } from "~/components/ui/button";
import { ButtonGroup } from "~/components/ui/button-group";
function ButtonGroupNavigation() {
return (
<Example title="Navigation">
<ButtonGroup>
<ButtonGroup>
<Button variant="outline">
<ArrowLeft />
</Button>
<Button variant="outline">
<ArrowRight />
</Button>
</ButtonGroup>
<ButtonGroup aria-label="Single navigation button">
<Button variant="outline" size="icon">
<ArrowLeft />
</Button>
</ButtonGroup>
</ButtonGroup>
</Example>
);
}

Text Alignment

import { Button } from "~/components/ui/button";
import { ButtonGroup } from "~/components/ui/button-group";
import { Field } from "~/components/ui/field";
import { Label } from "~/components/ui/label";
function ButtonGroupTextAlignment() {
return (
<Example title="Text Alignment">
<Field>
<Label id="alignment-label">Text Alignment</Label>
<ButtonGroup aria-labelledby="alignment-label">
<Button variant="outline" size="sm">
Left
</Button>
<Button variant="outline" size="sm">
Center
</Button>
<Button variant="outline" size="sm">
Right
</Button>
<Button variant="outline" size="sm">
Justify
</Button>
</ButtonGroup>
</Field>
</Example>
);
}

Vertical

import { Minus, Plus } from "lucide-solid";
import { Button } from "~/components/ui/button";
import { ButtonGroup } from "~/components/ui/button-group";
function ButtonGroupVertical() {
return (
<Example title="Vertical">
<div class="flex gap-6">
<ButtonGroup orientation="vertical" aria-label="Media controls" class="h-fit">
<Button variant="outline" size="icon">
<Plus />
</Button>
<Button variant="outline" size="icon">
<Minus />
</Button>
</ButtonGroup>
</div>
</Example>
);
}

Vertical Nested

import {
Copy,
FlipHorizontal,
FlipVertical,
RotateCw,
Search,
Share,
Trash,
} from "lucide-solid";
import { Button } from "~/components/ui/button";
import { ButtonGroup } from "~/components/ui/button-group";
function ButtonGroupVerticalNested() {
return (
<Example title="Vertical Nested">
<ButtonGroup orientation="vertical" aria-label="Design tools palette">
<ButtonGroup orientation="vertical">
<Button variant="outline" size="icon">
<Search />
</Button>
<Button variant="outline" size="icon">
<Copy />
</Button>
<Button variant="outline" size="icon">
<Share />
</Button>
</ButtonGroup>
<ButtonGroup orientation="vertical">
<Button variant="outline" size="icon">
<FlipHorizontal />
</Button>
<Button variant="outline" size="icon">
<FlipVertical />
</Button>
<Button variant="outline" size="icon">
<RotateCw />
</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline" size="icon">
<Trash />
</Button>
</ButtonGroup>
</ButtonGroup>
</Example>
);
}

API Reference

ButtonGroup

The ButtonGroup component is a container that groups related buttons together with consistent styling.

PropTypeDefault
orientation"horizontal" | "vertical""horizontal"
<ButtonGroup>
<Button>Button 1</Button>
<Button>Button 2</Button>
</ButtonGroup>

Nest multiple button groups to create complex layouts with spacing. See the nested example for more details.

<ButtonGroup>
<ButtonGroup />
<ButtonGroup />
</ButtonGroup>

ButtonGroupSeparator

The ButtonGroupSeparator component visually divides buttons within a group.

PropTypeDefault
orientation"horizontal" | "vertical""vertical"
<ButtonGroup>
<Button>Button 1</Button>
<ButtonGroupSeparator />
<Button>Button 2</Button>
</ButtonGroup>

ButtonGroupText

Use this component to display text within a button group.

<ButtonGroup>
<ButtonGroupText>Text</ButtonGroupText>
<Button>Button</Button>
</ButtonGroup>

Use a Label component inside ButtonGroupText to associate it with an input.

import { ButtonGroup, ButtonGroupText } from "~/components/ui/button-group";
import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
export function ButtonGroupTextDemo() {
return (
<ButtonGroup>
<ButtonGroupText>
<Label for="name">Text</Label>
</ButtonGroupText>
<Input placeholder="Type something here..." id="name" />
</ButtonGroup>
);
}