ButtonGroup

Groups related buttons. Detached by default; attached joins them into a segmented control.

Actions ButtonGroup.jsx ButtonGroup.d.ts ButtonGroup.md

Playground

Change a prop and watch it. The code below follows along.

ButtonGroup — Variants, sizes and states.

Install and import

ButtonGroup is in @dovetail-ds/react. Install it with React, if you haven't:

npm install @dovetail-ds/react react react-dom
import { ButtonGroup } from "@dovetail-ds/react";

Import @dovetail-ds/react/styles.css once at your app's root, then your theme after it. Setup covers the stylesheets, fonts and dark mode.

Guidelines

Groups related buttons. Detached by default; attached joins them into a segmented control.

Use it when

  • Two or more buttons act on the same object: a dialog footer, a toolbar cluster.
  • attached: mutually exclusive view options such as list/grid or day/week/month.

Don't use it when

  • The buttons are unrelated. Use Inline.
  • The options are a form value. Use Radio or Select.

Example

<ButtonGroup label="View mode" attached>
  <Button variant="secondary">List</Button>
  <Button variant="secondary">Grid</Button>
</ButtonGroup>

Accessibility

Renders role="group" with the required label. For a segmented control where one option is selected, set aria-pressed on each button yourself, because ButtonGroup does not manage selection state.

Props

The typed contract, from ButtonGroup.d.ts.

import * as React from "react";

/** Groups related buttons and manages their spacing or shared edges. */
export interface ButtonGroupProps extends React.HTMLAttributes<HTMLDivElement> {
  /** Accessible group name, e.g. "Text alignment". Required. */
  label: string;
  /** Join the buttons into one segmented control with shared edges. @default false */
  attached?: boolean;
  children?: React.ReactNode;
}

export declare function ButtonGroup(props: ButtonGroupProps): React.JSX.Element;

Tokens it reads

Every custom property ButtonGroup.jsx resolves, read from the source: 2 component, 1 semantic. Component tier first, because that is the one to reach for.

  • component Its own tier. Override one of these and only this component moves.
  • semantic Shared roles. Override one and everything using that role moves with it.
TokenTierDeclared as
--dt-button-border-widthcomponentvar(--dt-border-width-default)
--dt-button-radiuscomponentvar(--dt-radius-pill)
--dt-space-inline-xssemanticvar(--dt-dim-2)

Source

React only, no dependencies, every value a token.

import React from "react";

export function ButtonGroup({ label, attached = false, children, style, ...rest }) {
  const items = React.Children.toArray(children);
  return (
    <div
      role="group"
      aria-label={label}
      style={{ display: "inline-flex", gap: attached ? 0 : "var(--dt-space-inline-xs)", ...style }}
      {...rest}
    >
      {attached
        ? items.map((child, i) =>
            React.isValidElement(child)
              ? React.cloneElement(child, {
                  key: i,
                  style: {
                    borderRadius: i === 0
                      ? "var(--dt-button-radius) 0 0 var(--dt-button-radius)"
                      : i === items.length - 1
                        ? "0 var(--dt-button-radius) var(--dt-button-radius) 0"
                        : 0,
                    marginLeft: i > 0 ? "calc(-1 * var(--dt-button-border-width))" : 0,
                    ...(child.props.style || {}),
                  },
                })
              : child
          )
        : items}
    </div>
  );
}

In context

ButtonGroup also appears on the actions reference cards, alongside the rest of its family.