Toggle Group

A set of two-state buttons that can be toggled on or off.

import { Bold, Italic, Underline } from "lucide-react"

import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

export default function ToggleGroupDemo() {
  return (
    <ToggleGroup aria-label="Formatting" variant="outline" multiple>
      <ToggleGroupItem value="bold" aria-label="Toggle bold">
        <Bold />
      </ToggleGroupItem>
      <ToggleGroupItem value="italic" aria-label="Toggle italic">
        <Italic />
      </ToggleGroupItem>
      <ToggleGroupItem value="underline" aria-label="Toggle underline">
        <Underline />
      </ToggleGroupItem>
    </ToggleGroup>
  )
}

Installation

Install the following dependencies:

pnpm add @base-ui/react @stylexjs/stylex

Set up the token layer and the StyleX compiler. See Installation for states.ts, state-styles.ts and interop.css, which sizes icons. Install Toggle and the Button it depends on. Toggle provides the items and the shared group contexts.

Copy and paste the following code into your project.

components/ui/toggle-group.tsx
"use client"

import { use, type CSSProperties } from "react"
import { ToggleGroup as ToggleGroupPrimitive } from "@base-ui/react/toggle-group"
import * as stylex from "@stylexjs/stylex"
import type { StyleXStyles } from "@stylexjs/stylex"

import { ButtonGroupContext } from "@/components/ui/button"
import {
  Toggle,
  ToggleGroupContext,
  type ToggleProps,
  type ToggleSize,
  type ToggleVariant,
} from "@/components/ui/toggle"
import { stateStyles } from "@/lib/state-styles"
import { states } from "@/theme/states"
import { color, radius, space } from "@/theme/tokens.stylex"

const styles = stylex.create({
  root: { display: "flex", width: "fit-content", alignItems: "stretch" },
  vertical: { flexDirection: "column" },
  segmented: {
    "--carbon-toggle-group-radius": radius.lg,
    borderRadius: "var(--carbon-toggle-group-radius)",
    padding: "3px",
    color: color.mutedForeground,
  },
})
const spacingStyles = stylex.create({
  0: { gap: space.s0 },
  0.5: { gap: space.s0_5 },
  1: { gap: space.s1 },
  1.5: { gap: space.s1_5 },
  2: { gap: space.s2 },
  3: { gap: space.s3 },
  4: { gap: space.s4 },
  5: { gap: space.s5 },
  6: { gap: space.s6 },
  8: { gap: space.s8 },
  10: { gap: space.s10 },
  12: { gap: space.s12 },
})
const toggleGroupStyles = {
  root: styles.root,
  vertical: styles.vertical,
  spacing: spacingStyles,
  segmented: [states.well, styles.segmented],
}

type ToggleGroupProps<Value extends string> = Omit<
  ToggleGroupPrimitive.Props<Value>,
  "className" | "style"
> & {
  variant?: ToggleVariant
  size?: ToggleSize
  spacing?: keyof typeof spacingStyles
  xstyle?: StyleXStyles
  className?: string
  style?: CSSProperties
}

function ToggleGroup<Value extends string = string>({
  variant,
  size,
  spacing = 2,
  orientation = "horizontal",
  children,
  xstyle,
  className,
  style,
  ...props
}: ToggleGroupProps<Value>) {
  const parent = use(ToggleGroupContext)
  const segments = spacing === 0 && (variant ?? "default") === "default"
  const segmented = segments && !parent?.onWell
  const onWell = segments || !!parent?.onWell
  return (
    <ToggleGroupContext value={{ variant, size, onWell, segmented }}>
      <ButtonGroupContext value={spacing === 0 && !segments ? { orientation } : null}>
        <ToggleGroupPrimitive
          data-slot="toggle-group"
          data-variant={variant ?? "default"}
          data-size={size ?? "default"}
          data-spacing={spacing}
          data-orientation={orientation}
          orientation={orientation}
          {...props}
          {...stateStyles(
            (state) => [
              styles.root,
              state.orientation === "vertical" && styles.vertical,
              spacingStyles[spacing],
              segmented && [states.well, styles.segmented],
              xstyle,
            ],
            { className, style },
          )}
        >
          {children}
        </ToggleGroupPrimitive>
      </ButtonGroupContext>
    </ToggleGroupContext>
  )
}

function ToggleGroupItem<Value extends string = string>(props: ToggleProps<Value>) {
  return <Toggle data-slot="toggle-group-item" {...props} />
}

export { ToggleGroup, ToggleGroupItem, toggleGroupStyles }

Update the import paths to match your project setup.

Usage

import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

;<ToggleGroup aria-label="Options" defaultValue={["a"]}>
  <ToggleGroupItem value="a">A</ToggleGroupItem>
  <ToggleGroupItem value="b">B</ToggleGroupItem>
  <ToggleGroupItem value="c">C</ToggleGroupItem>
</ToggleGroup>

Pass multiple to allow more than one selection. value and defaultValue are arrays in both modes; use onValueChange to control the selection.

RTL support is not included.

Composition

Use ToggleGroupItem for each option.

ToggleGroup
├── ToggleGroupItem
└── ToggleGroupItem

Outline

Use variant="outline" for an outline style.

import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

export default function ToggleGroupOutline() {
  return (
    <ToggleGroup aria-label="Calls" variant="outline" defaultValue={["all"]}>
      <ToggleGroupItem value="all" aria-label="Toggle all">
        All
      </ToggleGroupItem>
      <ToggleGroupItem value="missed" aria-label="Toggle missed">
        Missed
      </ToggleGroupItem>
    </ToggleGroup>
  )
}

Size

Use size to set the size of every item.

import { VStack } from "@/components/ui/stack"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

export default function ToggleGroupSizes() {
  return (
    <VStack gap={4}>
      {(["sm", "default", "lg"] as const).map((size) => (
        <ToggleGroup
          key={size}
          aria-label={`${size} position`}
          size={size}
          defaultValue={["top"]}
          variant="outline"
        >
          <ToggleGroupItem value="top">Top</ToggleGroupItem>
          <ToggleGroupItem value="bottom">Bottom</ToggleGroupItem>
          <ToggleGroupItem value="left">Left</ToggleGroupItem>
          <ToggleGroupItem value="right">Right</ToggleGroupItem>
        </ToggleGroup>
      ))}
    </VStack>
  )
}

Segmented

With spacing={0} and the default variant, the group is a segmented control. The items sit in a shallow well and the pressed one is raised out of it. Inside a Toolbar the toolbar is the well, so the group draws none of its own.

import { VStack } from "@/components/ui/stack"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

export default function ToggleGroupSegmented() {
  return (
    <VStack gap={4}>
      {(["sm", "default", "lg"] as const).map((size) => (
        <ToggleGroup
          key={size}
          aria-label={`${size} view`}
          size={size}
          spacing={0}
          defaultValue={["list"]}
        >
          <ToggleGroupItem value="list">List</ToggleGroupItem>
          <ToggleGroupItem value="board">Board</ToggleGroupItem>
          <ToggleGroupItem value="calendar">Calendar</ToggleGroupItem>
        </ToggleGroup>
      ))}
    </VStack>
  )
}

Spacing

With variant="outline", use spacing={0} to join the items, or a spacing step to separate them.

import { VStack } from "@/components/ui/stack"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

export default function ToggleGroupSpacing() {
  return (
    <VStack gap={4}>
      {([0, 2] as const).map((spacing) => (
        <ToggleGroup
          key={spacing}
          aria-label={spacing === 0 ? "Joined position" : "Spaced position"}
          size="sm"
          defaultValue={["top"]}
          variant="outline"
          spacing={spacing}
        >
          <ToggleGroupItem value="top">Top</ToggleGroupItem>
          <ToggleGroupItem value="bottom">Bottom</ToggleGroupItem>
          <ToggleGroupItem value="left">Left</ToggleGroupItem>
          <ToggleGroupItem value="right">Right</ToggleGroupItem>
        </ToggleGroup>
      ))}
    </VStack>
  )
}

Vertical

Use orientation="vertical" to stack items and navigate with the up and down arrow keys.

import { BoldIcon, ItalicIcon, UnderlineIcon } from "lucide-react"

import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

export default function ToggleGroupVertical() {
  return (
    <ToggleGroup
      multiple
      orientation="vertical"
      spacing={0}
      variant="outline"
      defaultValue={["bold", "italic"]}
    >
      <ToggleGroupItem value="bold" aria-label="Toggle bold">
        <BoldIcon />
      </ToggleGroupItem>
      <ToggleGroupItem value="italic" aria-label="Toggle italic">
        <ItalicIcon />
      </ToggleGroupItem>
      <ToggleGroupItem value="underline" aria-label="Toggle underline">
        <UnderlineIcon />
      </ToggleGroupItem>
    </ToggleGroup>
  )
}

Disabled

Use disabled on the group to disable every item, or on an item to disable only that option.

import { VStack } from "@/components/ui/stack"
import { Bold, Italic, Underline } from "lucide-react"

import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

export default function ToggleGroupDisabled() {
  return (
    <VStack gap={4}>
      <ToggleGroup aria-label="Formatting" disabled>
        <ToggleGroupItem value="bold" aria-label="Toggle bold">
          <Bold />
        </ToggleGroupItem>
        <ToggleGroupItem value="italic" aria-label="Toggle italic">
          <Italic />
        </ToggleGroupItem>
        <ToggleGroupItem value="underline" aria-label="Toggle underline">
          <Underline />
        </ToggleGroupItem>
      </ToggleGroup>
      <ToggleGroup aria-label="Available formatting" multiple>
        <ToggleGroupItem value="bold" aria-label="Toggle bold">
          <Bold />
        </ToggleGroupItem>
        <ToggleGroupItem value="italic" aria-label="Toggle italic" disabled>
          <Italic />
        </ToggleGroupItem>
        <ToggleGroupItem value="underline" aria-label="Toggle underline">
          <Underline />
        </ToggleGroupItem>
      </ToggleGroup>
    </VStack>
  )
}

Custom

Use xstyle to customize the items and value to keep one weight selected.

Font Weight

Use font-normal to set the font weight.

"use client"

import { useId, useState } from "react"
import * as stylex from "@stylexjs/stylex"
import { Field, FieldDescription, FieldTitle } from "@/components/ui/field"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { color, leading, radius, space, text } from "@/theme/tokens.stylex"

const styles = stylex.create({
  item: { width: space.s16, height: space.s16, flexDirection: "column", borderRadius: radius.xl },
  sample: { fontSize: text.xl2, lineHeight: leading.xl2 },
  label: { fontSize: text.xs, lineHeight: leading.xs, color: color.mutedForeground },
})
const weights = stylex.create({
  light: { fontWeight: 300 },
  normal: { fontWeight: 400 },
  medium: { fontWeight: 500 },
  bold: { fontWeight: 700 },
})

export default function ToggleGroupFontWeightSelector() {
  const id = useId()
  const [fontWeight, setFontWeight] = useState<keyof typeof weights>("normal")
  return (
    <Field>
      <FieldTitle id={id}>Font Weight</FieldTitle>
      <ToggleGroup
        aria-labelledby={id}
        value={[fontWeight]}
        onValueChange={(value) => {
          if (value[0]) setFontWeight(value[0])
        }}
        variant="outline"
        spacing={2}
        size="lg"
      >
        {(["light", "normal", "medium", "bold"] as const).map((weight) => (
          <ToggleGroupItem key={weight} value={weight} aria-label={weight} xstyle={styles.item}>
            <span {...stylex.props(styles.sample, weights[weight])}>Aa</span>
            <span {...stylex.props(styles.label)}>
              {weight.charAt(0).toUpperCase() + weight.slice(1)}
            </span>
          </ToggleGroupItem>
        ))}
      </ToggleGroup>
      <FieldDescription>
        Use <code>font-{fontWeight}</code> to set the font weight.
      </FieldDescription>
    </Field>
  )
}

API reference

ToggleGroup accepts Base UI Toggle Group props. ToggleGroupItem accepts Toggle props. Both take render, xstyle, className and real inline style.

ToggleGroup

PropTypeDefault
variant"default" | "outline""default"
size"sm" | "default" | "lg""default"
spacing0 | 0.5 | 1 | 1.5 | 2 | 3 | 4 | 5 | 6 | 8 | 10 | 122

Group-level variant and size override item values. When omitted, each item uses its own value or its default.

toggleGroupStyles

KeyWhat it is
rootFitted row with stretching items
verticalColumn layout
spacingGaps on the token scale
segmentedThe well a segmented group draws

Data attributes

AttributeOnEffect
data-slot="toggle-group"rootTargets the group
data-slot="toggle-group-item"itemTargets an option
data-variant, data-sizeroot and itemsMirror the variant and size
data-spacingrootMirrors spacing
data-orientationrootMirrors orientation
data-disabledroot and itemsBase UI writes the disabled state
data-multiplerootBase UI writes it when multiple selection is enabled
data-presseditemsBase UI writes the selection state