Forms

Button Group.

Joins buttons, inputs and text into one control with shared borders, laid out horizontally or vertically.

Installation#

With the CLI

$npx lanterncn add button-group

This 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

  1. Install the dependencies

    npm install radix-ui class-variance-authority
  2. Copy the source into your project

    components/ui/button-group.tsx
    import * 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 };
  3. Update the import paths to match your project

    The source imports cn from @/lib/utils and 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

ch 42

Split button