Button Group

Horizontally joined button sets for related actions — solid, outline, icon-only, and size variants with proper border handling.

Preview

Basic button group

import { ButtonGroup, ButtonGroupItem } from '@/registry/ui-kit/tailwind-button-group/react'

function Example() {
  return (
    <ButtonGroup>
      <ButtonGroupItem>Left</ButtonGroupItem>
      <ButtonGroupItem>Center</ButtonGroupItem>
      <ButtonGroupItem>Right</ButtonGroupItem>
    </ButtonGroup>
  )
}

Outline variant

import { ButtonGroup, ButtonGroupItem } from '@/registry/ui-kit/tailwind-button-group/react'

function Example() {
  return (
    <ButtonGroup outline>
      <ButtonGroupItem>Day</ButtonGroupItem>
      <ButtonGroupItem>Week</ButtonGroupItem>
      <ButtonGroupItem>Month</ButtonGroupItem>
    </ButtonGroup>
  )
}

With active state

import { ButtonGroup, ButtonGroupItem } from '@/registry/ui-kit/tailwind-button-group/react'

function Example() {
  return (
    <ButtonGroup outline>
      <ButtonGroupItem>List</ButtonGroupItem>
      <ButtonGroupItem active>Grid</ButtonGroupItem>
      <ButtonGroupItem>Table</ButtonGroupItem>
    </ButtonGroup>
  )
}

Component API

PropDefaultDescription
childrenButtonGroupItem elements to render inside the group.
size"md"Size applied to all buttons in the group.
variant"primary"Color variant applied to all buttons.
outlinefalseRender outline-style buttons.

Source Code

"use client";

import { forwardRef, createContext, useContext } from "react";

/* ── Context ── */

type GroupVariant = "primary" | "secondary" | "danger";
type GroupSize = "sm" | "md" | "lg";

interface GroupContext {
  variant: GroupVariant;
  size: GroupSize;
  outline: boolean;
}

const Ctx = createContext<GroupContext>({ variant: "primary", size: "md", outline: false });

/* ── Variant styles ── */

const solidStyles: Record<GroupVariant, string> = {
  primary: "bg-indigo-600 text-white hover:bg-indigo-500",
  secondary: "bg-zinc-600 text-white hover:bg-zinc-500",
  danger: "bg-rose-600 text-white hover:bg-rose-500",
};

const outlineStyles: Record<GroupVariant, string> = {
  primary: "border-indigo-300 text-indigo-600 hover:bg-indigo-50 dark:border-indigo-800 dark:text-indigo-400 dark:hover:bg-indigo-950",
  secondary: "border-zinc-300 text-zinc-600 hover:bg-zinc-50 dark:border-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-900",
  danger: "border-rose-300 text-rose-600 hover:bg-rose-50 dark:border-rose-800 dark:text-rose-400 dark:hover:bg-rose-950",
};

const activeStyles: Record<GroupVariant, string> = {
  primary: "bg-indigo-600 text-white",
  secondary: "bg-zinc-600 text-white",
  danger: "bg-rose-600 text-white",
};

const sizeStyles: Record<GroupSize, string> = {
  sm: "px-3 py-1.5 text-xs",
  md: "px-4 py-2 text-sm",
  lg: "px-5 py-2.5 text-base",
};

/* ── ButtonGroup ── */

interface ButtonGroupProps {
  variant?: GroupVariant;
  size?: GroupSize;
  outline?: boolean;
  children: React.ReactNode;
  className?: string;
}

function ButtonGroup({ variant = "primary", size = "md", outline = false, children, className = "" }: ButtonGroupProps) {
  return (
    <Ctx.Provider value={{ variant, size, outline }}>
      <div
        role="group"
        className={`inline-flex ${outline ? "border rounded-lg divide-x divide-inherit" : "shadow-sm rounded-lg"} ${outline ? (variant === "primary" ? "border-indigo-300 dark:border-indigo-800" : variant === "danger" ? "border-rose-300 dark:border-rose-800" : "border-zinc-300 dark:border-zinc-700") : ""} ${className}`}
      >
        {children}
      </div>
    </Ctx.Provider>
  );
}

/* ── ButtonGroupItem ── */

interface ButtonGroupItemProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  active?: boolean;
  children: React.ReactNode;
}

const ButtonGroupItem = forwardRef<HTMLButtonElement, ButtonGroupItemProps>(
  ({ active = false, className = "", children, ...props }, ref) => {
    const { variant, size, outline } = useContext(Ctx);

    const base = "inline-flex items-center justify-center font-medium transition-all duration-150 focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 disabled:pointer-events-none disabled:opacity-50 first:rounded-l-lg last:rounded-r-lg";

    const colorClass = active
      ? activeStyles[variant]
      : outline
        ? outlineStyles[variant]
        : solidStyles[variant];

    return (
      <button
        ref={ref}
        className={`${base} ${sizeStyles[size]} ${colorClass} ${className}`}
        {...props}
      >
        {children}
      </button>
    );
  }
);

ButtonGroupItem.displayName = "ButtonGroupItem";

export { ButtonGroup, ButtonGroupItem };
export type { ButtonGroupProps, ButtonGroupItemProps, GroupVariant, GroupSize };
export default ButtonGroup;