← All components

ButtonGroup

Neighbouring buttons fused into one control, sharing a single hairline.

The idea

Related buttons that merely sit close read as several controls; fused, with their inner corners squared and one shared edge between them, they read as one control with parts. The shared edge comes from pulling each button half a pixel over its neighbour rather than deleting a border, so a hovered button's darker edge still shows on both of its sides.

Composes Button.

Install

terminal1 line
npm i mihirmodi-ui motion

The tokens, the theme script and the routing provider are a one-time job — see Setup. Or skip the package entirely and take the source at the foot of this page.

Usage

ButtonGroup — usage7 lines
import { Button, ButtonGroup } from "mihirmodi-ui";

<ButtonGroup label="History">
  <Button>Undo</Button>
  <Button>Redo</Button>
  <Button>History</Button>
</ButtonGroup>

Props

PropNotes
labelstringThe group's accessible name.
orientation"horizontal" | "vertical"Which edges fuse.

Source

Exactly what the package ships — this is read out of the source at build time, not a copy. Take it and adapt it if you would rather not add a dependency.

packages/ui/src/components/actions/ButtonGroup.tsx35 lines
import type { ReactNode } from "react";

export type ButtonGroupProps = {
  children: ReactNode;
  /** The group's accessible name — "Text alignment", "Pagination". */
  label?: string;
  orientation?: "horizontal" | "vertical";
  className?: string;
};

/**
 * Related buttons fused into one object: the inner corners square off and the
 * neighbours share a single hairline, so the row reads as one control with
 * several parts rather than several controls that happen to be close.
 *
 * Built for `secondary` buttons, whose hairline is what gets shared. The
 * shared edge is drawn by pulling each button half a pixel over its
 * neighbour, not by removing a border, so a hovered button's darker edge
 * still shows on both of its sides.
 */
export function ButtonGroup({ children, label, orientation = "horizontal", className = "" }: ButtonGroupProps) {
  const fuse =
    orientation === "horizontal"
      ? "flex-row [&>*:not(:first-child)]:-ml-[0.5px] [&>*:not(:first-child)]:rounded-l-none [&>*:not(:last-child)]:rounded-r-none"
      : "flex-col [&>*:not(:first-child)]:-mt-[0.5px] [&>*:not(:first-child)]:rounded-t-none [&>*:not(:last-child)]:rounded-b-none";
  return (
    <div
      role="group"
      aria-label={label}
      className={`inline-flex ${fuse} [&>*:hover]:z-10 [&>*:focus-visible]:z-20 [&>*]:relative ${className}`}
    >
      {children}
    </div>
  );
}