Skip to main content
Gremorie

Button Group

Cluster of buttons (or selects) joined by a shared border, with optional separators and label text.

Overview

ButtonGroup joins multiple Button siblings into a single visual unit by neutralizing inner borders and rounding only the first and last child. Use it for related actions that share a context: copy / paste / cut, alignment, view modes, pagination.

It also accepts Select triggers and ButtonGroupText so you can mix labels and controls in the same border-shared row. ButtonGroupSeparator adds a hairline between groups when you need a visual beat.

Preview

Installation

bash npx gremorie@latest add rx-button-group

bash pnpm dlx gremorie@latest add rx-button-group

bash yarn dlx gremorie@latest add rx-button-group

bash bunx --bun gremorie@latest add rx-button-group

Usage

import {
  Button,
  ButtonGroup,
  ButtonGroupSeparator,
} from "@gremorie/rx-forms";

export function Example() {
  return (
    <ButtonGroup>
      <Button variant="outline">Copy</Button>
      <ButtonGroupSeparator />
      <Button variant="outline">Paste</Button>
    </ButtonGroup>
  );
}

Angular edition planned for Phase 5h. Star the repo to track progress.

API

<ButtonGroup>

PropTypeDefaultDescription
orientation"horizontal" | "vertical""horizontal"Lays children side by side or stacked. Borders adapt: horizontal strips border-l, vertical strips border-t.

Extends all React.ComponentProps<"div">. Always rendered with role="group" and data-slot="button-group".

<ButtonGroupSeparator>

PropTypeDefaultDescription
orientation"horizontal" | "vertical""vertical"Radix Separator direction. Defaults to a vertical 1px line for horizontal groups.

Wraps @radix-ui/react-separator. Decorative (forced decorative flag), so it does not announce to screen readers.

<ButtonGroupText>

PropTypeDefaultDescription
asChildbooleanfalseRender as Slot.Root so the first child receives the muted/bordered surface. Use for non-interactive labels (e.g. "of 12", a unit).

Extends all React.ComponentProps<"div">.

Composition

  1. <ButtonGroup> strips inner borders and rounds only the first / last child via CSS sibling selectors.
  2. Children can be: <Button>, <Select> triggers (when wrapped in SelectTrigger), or <ButtonGroupText> for labels.
  3. <ButtonGroupSeparator> inserts a visible 1px divider between children that share a border.
  4. <ButtonGroupText> is the right element for static labels inside the cluster - never use a raw <span> or <div> since the group expects its children to participate in border merging.

Variations

Three actions with separators

Use for related commands. Separators communicate that each action is distinct, even though they share a visual surface.

<ButtonGroup>
  <Button variant="outline">Copy</Button>
  <ButtonGroupSeparator />
  <Button variant="outline">Paste</Button>
  <ButtonGroupSeparator />
  <Button variant="outline">Cut</Button>
</ButtonGroup>

Vertical group for sidebars

Switch orientation to stack actions in a compact column - useful for image editors, video players, and any tool palette.

import { Bold, Italic, Underline } from 'lucide-react';

<ButtonGroup orientation="vertical">
  <Button size="icon" variant="outline" aria-label="Bold">
    <Bold />
  </Button>
  <Button size="icon" variant="outline" aria-label="Italic">
    <Italic />
  </Button>
  <Button size="icon" variant="outline" aria-label="Underline">
    <Underline />
  </Button>
</ButtonGroup>;

Pagination with label text

Combine ButtonGroupText with arrow buttons. The text stays non-interactive but inherits the group's surface.

import { ChevronLeft, ChevronRight } from 'lucide-react';

<ButtonGroup>
  <Button variant="outline" size="icon" aria-label="Previous page">
    <ChevronLeft />
  </Button>
  <ButtonGroupText>Page 3 of 12</ButtonGroupText>
  <Button variant="outline" size="icon" aria-label="Next page">
    <ChevronRight />
  </Button>
</ButtonGroup>;

Accessibility

  • Group semantics: the wrapper renders role="group". Provide aria-label (or aria-labelledby) so screen readers announce what the cluster represents.
  • Keyboard: each child remains an independent focusable element. Tab and Shift+Tab move between them; there is no roving tabindex (use ToggleGroup if you want that).
  • Focus: focused child elevates above its siblings (z-10) so the focus ring is never clipped by the adjacent button's border.
  • Separators: rendered as decorative so they do not appear in the accessibility tree.
  • Button - the building block
  • Toggle Group - same visual idea but with coordinated aria-pressed state and roving tabindex
  • Input Group - the input + addon equivalent
  • Select - allowed inside ButtonGroup when you need a dropdown trigger in the cluster

On this page