Toggle Group

A toggle group is a row of toggles that belong together. Pick one, like an alignment, or several, like text styles.

import { AlignCenter, AlignLeft, AlignRight, Bold, Italic, Underline } from "lucide-react";
import { Toggle } from "@/components/ui/toggle";
import { ToggleGroup } from "@/components/ui/toggle-group";

export default function ToggleGroupDemo() {
  return (
    <>
      <ToggleGroup aria-label="Text alignment" defaultValue={["left"]}>
        <Toggle size="icon" value="left" aria-label="Align left">
          <AlignLeft />
        </Toggle>
        <Toggle size="icon" value="center" aria-label="Align center">
          <AlignCenter />
        </Toggle>
        <Toggle size="icon" value="right" aria-label="Align right">
          <AlignRight />
        </Toggle>
      </ToggleGroup>
      <ToggleGroup multiple aria-label="Text style" defaultValue={["bold", "italic"]}>
        <Toggle size="icon" value="bold" aria-label="Bold">
          <Bold />
        </Toggle>
        <Toggle size="icon" value="italic" aria-label="Italic">
          <Italic />
        </Toggle>
        <Toggle size="icon" value="underline" aria-label="Underline">
          <Underline />
        </Toggle>
      </ToggleGroup>
      <ToggleGroup aria-label="Billing period" defaultValue={["monthly"]}>
        <Toggle value="monthly">Monthly</Toggle>
        <Toggle value="yearly">Yearly</Toggle>
      </ToggleGroup>
      <ToggleGroup disabled aria-label="Disabled" defaultValue={["one"]}>
        <Toggle value="one">One</Toggle>
        <Toggle value="two">Two</Toggle>
      </ToggleGroup>
    </>
  );
}

The code

The component is a single file, read from the repository when the site is built. Press Show code to see all of it.

import type { ComponentProps } from "react";
import { ToggleGroup as BaseToggleGroup } from "@base-ui/react/toggle-group";
import { cx } from "@/lib/cx";

// A row of Toggles that belong together. The toggles are attached: only the outer corners are
// round and neighbours overlap by 1px so borders don't double. The pressed (or hovered, or
// focused) one is lifted above its neighbours so its whole border shows.
// Give each Toggle a `value`. By default pressing one releases the others; add `multiple` to
// allow several. Give the group an aria-label.
const attached = [
  "[&>button]:relative [&>button]:rounded-none",
  "[&>button:first-child]:rounded-l-md [&>button:last-child]:rounded-r-md",
  "[&>button:not(:first-child)]:-ml-px",
  "[&>button:hover]:z-10 [&>button:focus-visible]:z-10 [&>button[data-pressed]]:z-10",
].join(" ");

export function ToggleGroup({
  className,
  ...props
}: Omit<ComponentProps<typeof BaseToggleGroup>, "className"> & { className?: string }) {
  return <BaseToggleGroup className={cx("inline-flex", attached, className)} {...props} />;
}

Props

PropTypeDefaultWhat it does
multiplebooleanfalseLets more than one toggle be pressed at a time.
defaultValuestring[]—The values that start pressed, when you let it manage its own state.
valuestring[]—The pressed values, when you control them yourself.
onValueChangefunction—Called with the new list of pressed values.
disabledbooleanfalseTurns off every toggle in the group.
valuestring—On each Toggle inside. The value that toggle stands for. Required.

Give the group an aria-label. Every other prop goes to the Base UI ToggleGroup underneath.

The thinking

Some choices are a set: left, center or right, bold and italic. A group makes the set visible as one control.

  • Built on Base UI: The group tracks which values are pressed. Each item is your Toggle with a value, so it looks and behaves the same as a lone Toggle.
  • One or several: By default pressing a toggle releases the others, like alignment. Add multiple and any number can be pressed, like bold, italic and underline.
  • Attached: The toggles share their borders, so the row reads as one segmented control, like a monthly and yearly switch on a pricing table.

Interface design

  • Segments: Only the outer corners are rounded, with the md radius, 6px. Neighbours overlap by 1px, so a border is never doubled.
  • Pressed: The pressed segment has its accent border drawn above its neighbours, so no part of it is covered.
  • Disabled: Every segment is a flat surface with a quiet label.

Interaction

  • Press: Click a segment, or press Enter or Space when it has focus. In a single-choice group, pressing one releases the one that was pressed.
  • Keyboard: The group is one stop in the tab order, and the arrow keys move between its segments.
  • Each segment: Hover, focus and the press animation are the same as a lone Toggle.

Empathy

  • Name the group: The group is announced as a group. Give it an aria-label such as Text alignment, as the demo does.
  • Name each icon: Icon-only segments need an aria-label each, like Align left.
  • Pressed is announced: Each segment reports whether it is pressed, so the state isn't carried by color alone.