For the complete documentation index, see llms.txt. Markdown variants are available by appending .md to documentation URLs. Homepage markdown is available at /llms.md/content.md. An agent skill is available at /.well-known/agent-skills/site-skill.md.

Button Group

The Button Group component from this design system, built on Base UI.

Installation

$ pnpm dlx shadcn@latest add @cobalt/button-group

Source

import { Button as ButtonPrimitive } from "@base-ui/react/button";
import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
import { cva } from "class-variance-authority";
import type { VariantProps } from "class-variance-authority";
import { cn } from "cn";

import { Separator } from "@/components/ui/separator";

const buttonGroupVariants = cva(
  "flex w-fit items-stretch *:focus-visible:relative *:focus-visible:z-10 has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
  {
    defaultVariants: {
      orientation: "horizontal",
    },
    variants: {
      orientation: {
        horizontal:
          "*:data-slot:rounded-r-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-md! [&>[data-slot]~[data-slot]]:rounded-l-none [&>[data-slot]~[data-slot]]:border-l-0",
        vertical:
          "flex-col *:data-slot:rounded-b-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-md! [&>[data-slot]~[data-slot]]:rounded-t-none [&>[data-slot]~[data-slot]]:border-t-0",
      },
    },
  }
);

function ButtonGroup({
  className,
  orientation,
  variant = "default",
  ...props
}: React.ComponentProps<"div"> &
  VariantProps<typeof buttonGroupVariants> & {
    variant?: "default" | "outline";
  }) {
  return (
    <div
      role="group"
      data-slot="button-group"
      data-orientation={orientation}
      className={cn(
        variant === "default"
          ? buttonGroupVariants({ orientation })
          : "isolate inline-flex w-fit items-stretch overflow-hidden rounded-[10px] border border-[var(--button-group-border,var(--border))] bg-popover shadow-[shadow:var(--shadow-control,0_1px_2px_0_rgb(0_0_0/0.05))] [&>[data-slot=button-group-item]+[data-slot=button-group-item]]:border-l [&>[data-slot=button-group-item]+[data-slot=button-group-item]]:border-border",
        className
      )}
      {...props}
    />
  );
}

function ButtonGroupItem({
  className,
  size = "default",
  selected = false,
  ...props
}: ButtonPrimitive.Props & {
  size?: "default" | "sm" | "icon" | "icon-sm";
  selected?: boolean;
}) {
  return (
    <ButtonPrimitive
      data-slot="button-group-item"
      aria-pressed={selected || undefined}
      className={cn(
        "inline-flex cursor-pointer items-center justify-center gap-1 bg-popover font-sans text-sm leading-5 font-medium whitespace-nowrap text-foreground select-none outline-none transition-[background-color,color] duration-150 ease-[ease] focus-visible:relative focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset hover:bg-[var(--surface-hover,var(--muted))] active:bg-[var(--surface-strong,var(--muted))] disabled:cursor-not-allowed disabled:bg-muted disabled:text-[color:var(--foreground-subtle,var(--muted-foreground))] motion-reduce:transition-none [&_svg]:shrink-0",
        size === "default" &&
          "h-[34px] px-3 [&_svg:not([class*='size-'])]:size-5",
        size === "sm" &&
          "h-[30px] px-2.5 [&_svg:not([class*='size-'])]:size-[18px]",
        size === "icon" && "size-[34px] [&_svg:not([class*='size-'])]:size-5",
        size === "icon-sm" &&
          "size-[30px] [&_svg:not([class*='size-'])]:size-[18px]",
        selected && "bg-[var(--surface-hover,var(--muted))]",
        className
      )}
      {...props}
    />
  );
}

function ButtonGroupText({
  className,
  render,
  ...props
}: useRender.ComponentProps<"div">) {
  return useRender({
    defaultTagName: "div",
    props: mergeProps<"div">(
      {
        className: cn(
          "flex items-center gap-2 rounded-[8px] border bg-muted px-2.5 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
          className
        ),
      },
      props
    ),
    render,
    state: {
      slot: "button-group-text",
    },
  });
}

function ButtonGroupSeparator({
  className,
  orientation = "vertical",
  ...props
}: React.ComponentProps<typeof Separator>) {
  return (
    <Separator
      data-slot="button-group-separator"
      orientation={orientation}
      className={cn(
        "relative self-stretch bg-input data-horizontal:mx-px data-horizontal:w-auto data-vertical:my-px data-vertical:h-auto",
        className
      )}
      {...props}
    />
  );
}

export {
  ButtonGroup,
  ButtonGroupItem,
  ButtonGroupSeparator,
  ButtonGroupText,
  buttonGroupVariants,
};