Bloom Logo

Button Group

ButtonGroup allows grouping multiple buttons together, creating a visually connected set. Propagates variants, colors, sizes, loading, and disabled states to child buttons.

importimport { ButtonGroup } from "@/components/ui/buttonGroup/buttonGroup";
$npx @bloomui-react/cli add buttonGroup
buttonGroup.tsx
"use client";

import * as React from "react";
import { cn } from "@/lib/utils";
import type { ButtonProps } from "../button/button";

export type ButtonGroupProps = {
  children: React.ReactNode;
  variant?: ButtonProps["variant"];
  color?: ButtonProps["color"];
  size?: ButtonProps["size"];
  radius?: ButtonProps["radius"];
  orientation?: "horizontal" | "vertical";
  isAttached?: boolean;
  isLoading?: boolean;
  isDisabled?: boolean;
  ariaLabel?: string;
  className?: string;
};

export const ButtonGroup = React.memo(
  ({
    children,
    variant,
    color,
    size,
    radius,
    orientation = "horizontal",
    isAttached = true,
    isLoading,
    isDisabled,
    ariaLabel,
    className,
  }: ButtonGroupProps) => {
    const clonedChildren = React.useMemo(() => {
      const childrenArray = React.Children.toArray(children);
      const count = childrenArray.length;
      return childrenArray.map((child, index) => {
        if (!React.isValidElement<ButtonProps>(child)) return child;

        const isFirst = index === 0;
        const isLast = index === count - 1;
        const isVertical = orientation === "vertical";

        const childRadius = child.props.radius || radius || "xl";
        let radiusClasses = "";

        if (isAttached) {
          if (isVertical) {
            if (isFirst)
              radiusClasses = `rounded-b-none rounded-t-${childRadius}`;
            else if (isLast)
              radiusClasses = `rounded-t-none rounded-b-${childRadius}`;
            else radiusClasses = "rounded-none";
          } else {
            if (isFirst)
              radiusClasses = `rounded-r-none rounded-l-${childRadius}`;
            else if (isLast)
              radiusClasses = `rounded-l-none rounded-r-${childRadius}`;
            else radiusClasses = "rounded-none";
          }
        }

        return React.cloneElement(child, {
          variant: child.props.variant || variant,
          color: child.props.color || color,
          size: child.props.size || size,
          radius: child.props.radius || radius,
          isLoading:
            child.props.isLoading !== undefined
              ? child.props.isLoading
              : isLoading,
          isDisabled:
            child.props.isDisabled !== undefined
              ? child.props.isDisabled
              : isDisabled,
          className: cn(
            child.props.className,
            isAttached && radiusClasses,
            "focus-visible:z-10 focus-visible:relative",
            isAttached && !isFirst && !isVertical && "-ml-px",
            isAttached && !isFirst && isVertical && "-mt-px",
          ),
        });
      });
    }, [
      children,
      variant,
      color,
      size,
      radius,
      orientation,
      isAttached,
      isLoading,
      isDisabled,
    ]);

    return (
      <div
        role="group"
        aria-label={ariaLabel}
        aria-orientation={orientation}
        className={cn(
          "inline-flex",
          orientation === "vertical"
            ? "flex-col items-stretch"
            : "flex-row items-center",
          !isAttached && (orientation === "vertical" ? "gap-2" : "gap-2"),
          className,
        )}
      >
        {clonedChildren}
      </div>
    );
  },
);

ButtonGroup.displayName = "ButtonGroup";

Default

A standard connected button group propagating default styling to its child buttons.

Variants

Defines the visual style of each button inside the group via the variant prop.

variant: default | bordered | light | flat | ghost | shadow | link
variant="default"
variant="bordered"
variant="light"
variant="flat"
variant="ghost"
variant="shadow"
variant="link"

Colors

Defines the color theme of all buttons inside the group via the color prop. Stacked vertically for clear visual comparison.

color: default | primary | secondary | accent | success | warning | danger
color="default"
default
bordered
light
flat
ghost
shadow
link
color="primary"
default
bordered
light
flat
ghost
shadow
link
color="secondary"
default
bordered
light
flat
ghost
shadow
link
color="accent"
default
bordered
light
flat
ghost
shadow
link
color="success"
default
bordered
light
flat
ghost
shadow
link
color="warning"
default
bordered
light
flat
ghost
shadow
link
color="danger"
default
bordered
light
flat
ghost
shadow
link

Sizes

Adjusts the size scale of each button in the group using the size prop.

size: xs | sm | md | lg | xl | 2xl | 3xl
size="xs"
size="sm"
size="md"
size="lg"
size="xl"
size="2xl"
size="3xl"

Radius

Controls the corner rounding of the outer borders of the group using the radius prop. Inner corners remain flat when buttons are attached.

radius: none | xs | sm | md | lg | xl | 2xl | 3xl | full
radius="none"
radius="sm" (with size="sm")
radius="md"
radius="full"

Loading state

Pass isLoading to ButtonGroup to propagate active loading spinners across all buttons in the group.

isLoading: boolean

Disabled State

Pass isDisabled to ButtonGroup to disable interaction for all buttons in the group.

isDisabled: boolean

Icon Only

Supports icon-only buttons grouped together seamlessly.

isIconOnly: booleanariaLabel: string

Vertical Orientation

Stack buttons vertically using orientation=vertical.

orientation: horizontal | vertical

Spaced Out Buttons

Set isAttached={false} to add clean spacing between buttons instead of merging borders.

isAttached: boolean

Props — ButtonGroup

Properties to configure the ButtonGroup container.

PropTypeDefaultDescription
variant'default' | 'bordered' | 'light' | 'flat' | 'ghost' | 'shadow' | 'link'Applies visual style variant to all child buttons.
color'default' | 'primary' | 'secondary' | 'accent' | 'success' | 'warning' | 'danger'Applies color theme to all child buttons.
size'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl'Applies size scale to all child buttons.
isLoadingbooleanfalsePropagates loading spinners and disables interaction across all buttons.
isDisabledbooleanfalseDisables interaction across all buttons in the group.
ariaLabelstringAccessible label describing the purpose of the group for screen readers.