Component

Button Group

A joined row of related buttons. Inner buttons share borders (side radius removed, borders overlapped by 1px) so the group reads as one control. Pass children for full control, or the `items` prop for a quick group.

  • Component
  • React
  • TSX
  • Tailwind CSS
  • MIT

Preview

Live component

Interactive preview
Open preview
9:41 100%
devsnips.dev/library/react/components/buttons/button-group/
fluid · no fixed width 100%
No dependencies Responsive · Light/dark · Focus-visible

Install

Add to your project

terminal
npx devsnips add React/Components/Buttons/button-group
registry path: React/Components/Buttons/button-group

Source

Implementation

code.tsx
import type { HTMLAttributes, ReactNode } from "react";

/* DevSnips React — ButtonGroup
 * Joined row of related buttons. Inner buttons lose their side radius and
 * overlap borders by 1px so the group reads as one control.
 */

export type ButtonSize = "xs" | "sm" | "md" | "lg" | "xl";
export type GroupVariant = "outline" | "solid" | "secondary" | "ghost";

export interface ButtonGroupItem {
  id?: string;
  label: ReactNode;
  icon?: string;
  active?: boolean;
  onClick?: () => void;
}

export interface ButtonGroupProps extends HTMLAttributes<HTMLDivElement> {
  items?: ButtonGroupItem[];
  variant?: GroupVariant;
  size?: ButtonSize;
  /** Accessible group name (rendered as aria-label). */
  label?: string;
}

function cx(...parts: Array<string | false | null | undefined>): string {
  return parts.filter(Boolean).join(" ");
}

const SIZES: Record<ButtonSize, string> = {
  xs: "h-7 gap-1 px-2 text-xs [&_svg]:size-[14px]",
  sm: "h-8 gap-1.5 px-3 text-xs [&_svg]:size-[14px]",
  md: "h-9 gap-2 px-3.5 text-[13px] [&_svg]:size-4",
  lg: "h-10 gap-2 px-4 text-[13px] [&_svg]:size-[18px]",
  xl: "h-11 gap-2 px-5 text-sm [&_svg]:size-5",
};

const VARIANTS: Record<GroupVariant, string> = {
  outline: "border-[var(--ds-color-border-strong)] bg-transparent text-[var(--ds-color-foreground)] hover:bg-[var(--ds-color-surface-hover)] active:bg-[var(--ds-color-surface-active)]",
  solid: "border-transparent bg-[var(--ds-color-primary)] text-[var(--ds-color-primary-foreground)] hover:bg-[color-mix(in_srgb,var(--ds-color-primary)_88%,#000)] active:bg-[color-mix(in_srgb,var(--ds-color-primary)_80%,#000)]",
  secondary: "border-[var(--ds-color-border)] bg-[var(--ds-color-secondary)] text-[var(--ds-color-secondary-foreground)] hover:bg-[var(--ds-color-surface-active)] active:bg-[var(--ds-color-surface-active)]",
  ghost: "border-transparent bg-transparent text-[var(--ds-color-foreground)] hover:bg-[var(--ds-color-surface-hover)] active:bg-[var(--ds-color-surface-active)]",
};

const BASE_BTN =
  "inline-flex select-none items-center justify-center whitespace-nowrap rounded-[var(--ds-radius-sm)] border font-medium leading-none " +
  "transition-colors duration-150 ease-out motion-reduce:transition-none " +
  "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ds-color-focus-ring)] " +
  "disabled:pointer-events-none disabled:opacity-50";

export function ButtonGroup({
  items,
  children,
  variant = "outline",
  size = "md",
  label,
  className,
  ...rest
}: ButtonGroupProps) {
  return (
    <div role="group" aria-label={label} className={cx("inline-flex", className)} {...rest}>
      {items
        ? items.map((it, i) => (
            <button
              key={it.id ?? i}
              type="button"
              aria-pressed={it.active || undefined}
              onClick={it.onClick}
              className={cx(
                BASE_BTN,
                VARIANTS[variant],
                SIZES[size],
                "rounded-none border-r-0",
                i === 0 ? "rounded-l-[var(--ds-radius-sm)]" : "-ml-px",
                i === items.length - 1 && "rounded-r-[var(--ds-radius-sm)] border-r",
                it.active && "bg-[var(--ds-color-surface-active)]",
              )}
            >
              {it.icon ? <Icon name={it.icon} className="shrink-0" /> : null}
              <span>{it.label}</span>
            </button>
          ))
        : children}
    </div>
  );
}

export default ButtonGroup;
89 lines UTF-8 · LF · Spaces: 2

AI-ready

Available to your agent.

This component is reachable through the DevSnips CLI, MCP server and Skills integrations — one registry, one resource path.

Resource path

devsnips://components/buttons/button-group

Continue browsing

View all