Skip to content

Button Group

Joins related Buttons into one visual unit with shared borders and rounded outer corners only, horizontal or vertical. Also the base for a split button and an icon toolbar.

Inputv0.1.0experimentalWCAG 2.2 AAView spec
import { Button, ButtonGroup } from "@rdloom/react";

export default function ButtonGroupBasicExample() {
  return (
    <div className="flex w-full justify-center">
      <ButtonGroup label="Calendar view">
        <Button variant="secondary">Day</Button>
        <Button variant="secondary">Week</Button>
        <Button variant="secondary">Month</Button>
      </ButtonGroup>
    </div>
  );
}

Installation

npx rdloom add button-group

Copies the source into src/components/rdloom. Edit it freely: rdloom upgrade merges later versions into your changes.

Use another registry client
npx shadcn@latest add https://rdloom.vimalbhatt.com/r/button-group.json

Works, but without upgrade tracking.

Usage

import { Button, ButtonGroup } from "@rdloom/react";

<div className="flex w-full justify-center">
  <ButtonGroup label="Calendar view">
    <Button variant="secondary">Day</Button>
    <Button variant="secondary">Week</Button>
    <Button variant="secondary">Month</Button>
  </ButtonGroup>
</div>

Split button

import { Button, ButtonGroup, Menu, MenuItem, MenuTrigger } from "@rdloom/react";

export default function ButtonGroupSplitButtonExample() {
  return (
    <div className="flex w-full justify-center">
      <ButtonGroup label="Publish">
        <Button variant="primary">Publish</Button>
        <MenuTrigger>
          <Button variant="primary" aria-label="More publish options" className="px-2">
            <svg aria-hidden="true" viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
              <path d="M4 6l4 4 4-4" />
            </svg>
          </Button>
          <Menu placement="bottom end">
            <MenuItem id="schedule">Schedule for later</MenuItem>
            <MenuItem id="draft">Save as draft</MenuItem>
            <MenuItem id="preview">Preview</MenuItem>
          </Menu>
        </MenuTrigger>
      </ButtonGroup>
    </div>
  );
}

Icon toolbar

import { Button, ButtonGroup } from "@rdloom/react";

const icon = { "aria-hidden": true, viewBox: "0 0 16 16", width: 16, height: 16, fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round" } as const;

export default function ButtonGroupIconToolbarExample() {
  return (
    <div className="flex w-full justify-center">
      <ButtonGroup label="Text alignment" size="sm">
        <Button variant="secondary" aria-label="Align left" className="aspect-square px-0!">
          <svg {...icon}><path d="M2 4h12M2 8h8M2 12h10" /></svg>
        </Button>
        <Button variant="secondary" aria-label="Align center" className="aspect-square px-0!">
          <svg {...icon}><path d="M2 4h12M4 8h8M3 12h10" /></svg>
        </Button>
        <Button variant="secondary" aria-label="Align right" className="aspect-square px-0!">
          <svg {...icon}><path d="M2 4h12M6 8h8M4 12h10" /></svg>
        </Button>
      </ButtonGroup>
    </div>
  );
}

API Reference

Defined by the spec. Components also accept the props of the React Aria component they wrap.

PropTypeDefault
labelrequired

Accessible name of the group, e.g. "Text formatting". Read when focus enters the group.

stringnone
orientation

Direction the buttons are stacked.

"horizontal" | "vertical""horizontal"
size

Size passed to every Button that does not set its own. A Button inside a MenuTrigger needs its own size.

"sm" | "md" | "lg"none
children

Two or more Buttons, or a MenuTrigger holding a Button.

nodenone

Accessibility

Role group, WCAG 2.2 AA. Tested with axe and keyboard tests; screen reader checks are in the audit checklist.

Keyboard

  • Tab moves through the buttons one by one, in order
  • Enter or Space activates the focused button
  • A menu button opens its menu with Enter, Space or Arrow Down

Screen readers announce

  • Announced as a group with its label, then each button by name
  • A menu button is announced as a menu button, collapsed or expanded

What your code must do

  • The group has role group and an accessible name from label
  • Every button keeps its own accessible name; an icon-only button needs an aria-label
  • The focused button is raised above its neighbours so its focus ring is never covered by a shared border
  • Buttons keep their own disabled and pressed states; the group does not hide them

Guidelines

Use it when

  • Closely related actions that belong together, like Bold, Italic, Underline
  • A split button: a main action next to a menu of alternatives
  • Previous and next style controls

Avoid it when

  • Choosing one option from several: use SegmentedControl or ToggleButtonGroup
  • Unrelated actions: leave space between them
  • Many buttons that do not fit in one row: use a Menu

Don't

  • Mixing several filled primary buttons in one group
  • Icon-only buttons without an aria-label
  • A split button whose menu item repeats the main action

Design tokens

The semantic tokens this component uses. Change them once and every component follows; see Design tokens.

  • --rd-color-border-default
  • --rd-color-border-strong
  • --rd-color-surface-default
  • --rd-color-surface-subtle
  • --rd-color-action-primary
  • --rd-color-focus-ring
  • --rd-radius-control