Monet
@etched/monet 0.0.1

Button group

Ported

Joins a few related buttons into one control, most often a split button: an action next to a menu of related actions. Inner corners are square and neighbors share one edge. It is a group: give it an aria-label, and an icon-only menu trigger its own. For a row of separate controls with arrow-key focus, use Toolbar.

Kumo source
import { CaretDownIcon } from "@phosphor-icons/react";
import { Button } from "@etched/monet/button";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@etched/monet/dropdown";
import { ButtonGroup } from "@etched/monet/button-group";

export function Preview() {
  return (
    <ButtonGroup aria-label="Run tests">
      <Button variant="primary">Run tests</Button>
      <DropdownMenu>
        <DropdownMenuTrigger render={<Button variant="primary" shape="square" aria-label="More run options" icon={<CaretDownIcon />} />} />
        <DropdownMenuContent align="end">
          <DropdownMenuItem>Run on bench A-03</DropdownMenuItem>
          <DropdownMenuItem>Run failed tests only</DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
    </ButtonGroup>
  );
}

Installation

import { ButtonGroup } from "@etched/monet/button-group";

Examples

Split button

An action and a menu of related actions. The caret is shape="square" with its own aria-label; filled buttons get a seam in a lighter shade.

import { CaretDownIcon } from "@phosphor-icons/react";
import { Button } from "@etched/monet/button";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@etched/monet/dropdown";
import { ButtonGroup } from "@etched/monet/button-group";

export function SplitButton() {
  return (
    <ButtonGroup aria-label="Run tests">
      <Button variant="primary">Run tests</Button>
      <DropdownMenu>
        <DropdownMenuTrigger render={<Button variant="primary" shape="square" aria-label="More run options" icon={<CaretDownIcon />} />} />
        <DropdownMenuContent align="end">
          <DropdownMenuItem>Run on bench A-03</DropdownMenuItem>
          <DropdownMenuItem>Run failed tests only</DropdownMenuItem>
          <DropdownMenuItem>Schedule run…</DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
    </ButtonGroup>
  );
}

Secondary

Any look works; give every button in the group the same one.

import { CaretDownIcon } from "@phosphor-icons/react";
import { Button } from "@etched/monet/button";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@etched/monet/dropdown";
import { ButtonGroup } from "@etched/monet/button-group";

export function Secondary() {
  return (
    <ButtonGroup aria-label="Save">
      <Button>Save</Button>
      <DropdownMenu>
        <DropdownMenuTrigger render={<Button shape="square" aria-label="More save options" icon={<CaretDownIcon />} />} />
        <DropdownMenuContent align="end">
          <DropdownMenuItem>Save as draft</DropdownMenuItem>
          <DropdownMenuItem>Save and close</DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
    </ButtonGroup>
  );
}

Sizes

Give every button in the group the same size.

import { CaretLeftIcon, CaretRightIcon } from "@phosphor-icons/react";
import { Button } from "@etched/monet/button";
import { ButtonGroup } from "@etched/monet/button-group";

export function Sizes() {
  return (
    <div className="row">
      {(["sm", "base"] as const).map((size) => (
        <ButtonGroup key={size} aria-label="Candidates">
          <Button size={size} shape="square" aria-label="Previous candidate" icon={<CaretLeftIcon />} />
          <Button size={size} shape="square" aria-label="Next candidate" icon={<CaretRightIcon />} />
        </ButtonGroup>
      ))}
    </div>
  );
}

With a field

A field and the button that acts on it.

import { Button } from "@etched/monet/button";
import { Input } from "@etched/monet/input";
import { ButtonGroup } from "@etched/monet/button-group";

export function WithAField() {
  return (
    <ButtonGroup aria-label="Invite">
      <Input type="email" aria-label="Email" placeholder="name@etched.com" />
      <Button>Invite</Button>
    </ButtonGroup>
  );
}

Accessibility

  • A group. Each button in it is its own Tab stop; arrow keys do not move between them (use Toolbar for that).
  • A split button's caret is a menu trigger with aria-expanded.
  • Name the group with aria-label, such as "Run tests". Give an icon-only caret its own aria-label, such as "More run options".

API reference

PropTypeDefaultDescription
childrenReactNode–A few buttons that belong together, often an action and a menu trigger. Each keeps its own variant and size; give them the same ones.