Forms
Button Group.
Joins buttons, inputs and text into one control with shared borders, laid out horizontally or vertically.
import { ArrowDownIcon, ArrowLeftIcon, ArrowRightIcon, ArrowUpIcon, RotateCcwIcon, RotateCwIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { ButtonGroup } from "@/components/ui/button-group";
export default function ButtonGroupDemo() {
return (
<div className="flex flex-wrap items-start justify-center gap-6">
<ButtonGroup aria-label="Move turtle">
<Button variant="outline" size="icon" aria-label="Turn left">
<RotateCcwIcon />
</Button>
<Button variant="outline">Forward</Button>
<Button variant="outline">Back</Button>
<Button variant="outline" size="icon" aria-label="Turn right">
<RotateCwIcon />
</Button>
</ButtonGroup>
<ButtonGroup orientation="vertical" aria-label="Dig direction">
<Button variant="outline" size="icon" aria-label="Dig up">
<ArrowUpIcon />
</Button>
<Button variant="outline" size="icon" aria-label="Dig forward">
<ArrowRightIcon />
</Button>
<Button variant="outline" size="icon" aria-label="Dig down">
<ArrowDownIcon />
</Button>
</ButtonGroup>
<ButtonGroup aria-label="Pages">
<Button variant="outline" size="sm">
<ArrowLeftIcon />
Prev
</Button>
<Button variant="outline" size="sm" className="text-primary">
3
</Button>
<Button variant="outline" size="sm">
Next
<ArrowRightIcon />
</Button>
</ButtonGroup>
</div>
);
}Installation#
With the CLI
$npx lanterncn add button-groupThis also installs the Lantern theme and any Lantern UI components it depends on. The shadcn CLI works directly too: npx shadcn@latest add httptim/lantern-ui/button-group. See Installation if your project is not set up yet.
Manually
Install the dependencies
npm install radix-ui class-variance-authorityCopy the source into your project
components/ui/button-group.tsximport * as React from "react"; import { cva, type VariantProps } from "class-variance-authority"; import { Slot } from "radix-ui"; import { cn } from "@/lib/utils"; import { Separator } from "@/components/ui/separator"; const buttonGroupVariants = cva( "flex w-fit items-stretch [&>*]:focus-visible:relative [&>*]:focus-visible:z-10 [&>*]:active:translate-y-0 [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md", { variants: { orientation: { horizontal: "[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-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" }, }, ); function ButtonGroup({ className, orientation, ...props }: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) { return ( <div role="group" data-slot="button-group" data-orientation={orientation ?? "horizontal"} className={cn(buttonGroupVariants({ orientation }), className)} {...props} /> ); } function ButtonGroupText({ className, asChild = false, ...props }: React.ComponentProps<"div"> & { asChild?: boolean }) { const Comp = asChild ? Slot.Root : "div"; return ( <Comp data-slot="button-group-text" className={cn( "flex items-center gap-2 rounded-md border border-input bg-secondary px-3 font-mono text-[10px] tracking-[0.2em] text-muted-foreground uppercase [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4", className, )} {...props} /> ); } function ButtonGroupSeparator({ className, orientation = "vertical", ...props }: React.ComponentProps<typeof Separator>) { return ( <Separator data-slot="button-group-separator" orientation={orientation} className={cn("relative !m-0 self-stretch bg-input data-[orientation=vertical]:h-auto", className)} {...props} /> ); } export { ButtonGroup, ButtonGroupSeparator, ButtonGroupText, buttonGroupVariants };Update the import paths to match your project
The source imports
cnfrom@/lib/utilsand uses separator.
Usage#
import { ButtonGroup, ButtonGroupSeparator, ButtonGroupText } from "@/components/ui/button-group";<ButtonGroup>
<Button variant="outline">Forward</Button>
<Button variant="outline">Back</Button>
</ButtonGroup>Use orientation="vertical" to stack. Nest ButtonGroups to put gaps between sets.
Filled buttons have no border, so put a ButtonGroupSeparator between them, as in the split button. Give the group an aria-label when its purpose is not clear from the buttons.
Examples#
With input
import { SendIcon } from "lucide-react";
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";
export default function ButtonGroupInput() {
return (
<div className="grid w-full max-w-sm gap-2.5">
<Label htmlFor="rednet-message">Rednet broadcast</Label>
<ButtonGroup className="w-full">
<ButtonGroupText>ch 42</ButtonGroupText>
<Input id="rednet-message" placeholder="Message to all turtles" />
<Button variant="outline" size="icon" aria-label="Send">
<SendIcon />
</Button>
</ButtonGroup>
</div>
);
}Split button
"use client";
import { ChevronDownIcon, DownloadIcon, HardDriveIcon, PlayIcon, RefreshCwIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { ButtonGroup, ButtonGroupSeparator } from "@/components/ui/button-group";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
export default function ButtonGroupSplit() {
return (
<div className="flex flex-wrap items-center justify-center gap-6">
<ButtonGroup>
<Button>
<PlayIcon />
Run excavate
</Button>
<ButtonGroupSeparator />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="icon" aria-label="More run options">
<ChevronDownIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-52">
<DropdownMenuLabel>Run on</DropdownMenuLabel>
<DropdownMenuItem>turtle-07</DropdownMenuItem>
<DropdownMenuItem>turtle-12</DropdownMenuItem>
<DropdownMenuItem>All miners</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<RefreshCwIcon />
Run on boot
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</ButtonGroup>
<ButtonGroup>
<Button variant="secondary">
<DownloadIcon />
Save
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="secondary" size="icon" aria-label="More save options">
<ChevronDownIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuItem>
<HardDriveIcon />
Save to disk
</DropdownMenuItem>
<DropdownMenuItem>Save as...</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</ButtonGroup>
</div>
);
}