Toggle

A two-state button that can be either on or off.

import { BookmarkIcon } from "lucide-react"

import { Toggle } from "@/components/ui/toggle"

export default function ToggleDemo() {
  return (
    <Toggle aria-label="Toggle bookmark" size="sm" variant="outline">
      <BookmarkIcon />
      Bookmark
    </Toggle>
  )
}

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 for icon sizing. Install Button for buttonStyles and ButtonGroupContext.

Copy and paste the following code into your project.

components/ui/toggle.tsx
"use client"

import { createContext, use, type CSSProperties } from "react"
import { Toggle as TogglePrimitive } from "@base-ui/react/toggle"
import * as stylex from "@stylexjs/stylex"
import type { StyleXStyles } from "@stylexjs/stylex"

import { ButtonGroupContext, buttonStyles } from "@/components/ui/button"
import { stateStyles } from "@/lib/state-styles"
import { states } from "@/theme/states"
import { color, radius, space } from "@/theme/tokens.stylex"

const pressedBg = "light-dark(oklch(0.93 0 0), oklch(0.3 0 0))"

const styles = stylex.create({
  pressed: {
    backgroundColor: pressedBg,
    color: color.foreground,
    "--carbon-button-highlight": color.transparent,
    "--carbon-button-drop": color.transparent,
  },
  pressedKey: {
    color: color.secondaryForeground,
    backgroundClip: "border-box",
    "--carbon-button-highlight": color.buttonSecondaryHighlight,
    "--carbon-button-drop": color.buttonSecondaryDrop,
  },
  onWell: { backgroundColor: { default: null, ":hover": color.itemHighlightBg } },
  segment: {
    "--carbon-button-radius": `calc(var(--carbon-toggle-group-radius, ${radius.lg}) - 3px)`,
  },
})
const minimums = stylex.create({
  default: { minWidth: space.s8 },
  sm: { minWidth: space.s7 },
  lg: { minWidth: space.s9 },
})
const variants = { default: buttonStyles.variants.ghost, outline: buttonStyles.variants.outline }
const sizes = {
  default: [buttonStyles.sizes.default, minimums.default],
  sm: [buttonStyles.sizes.sm, minimums.sm],
  lg: [buttonStyles.sizes.lg, minimums.lg],
}
const toggleStyles = {
  base: buttonStyles.base,
  variants,
  sizes,
  pressed: styles.pressed,
  pressedKey: [states.key, states.keySurface, styles.pressedKey],
  onWell: styles.onWell,
  pressedOnWell: states.wellKey,
  segment: styles.segment,
  disabled: states.disabled,
}
type ToggleVariant = keyof typeof variants
type ToggleSize = keyof typeof sizes

const ToggleGroupContext = createContext<{
  variant?: ToggleVariant
  size?: ToggleSize
  onWell?: boolean
  segmented?: boolean
} | null>(null)

type ToggleProps<Value extends string = string> = Omit<
  TogglePrimitive.Props<Value>,
  "className" | "style"
> & {
  variant?: ToggleVariant
  size?: ToggleSize
  xstyle?: StyleXStyles
  className?: string
  style?: CSSProperties
}

function Toggle<Value extends string = string>({
  variant,
  size,
  xstyle,
  className,
  style,
  ...props
}: ToggleProps<Value>) {
  const group = use(ToggleGroupContext)
  const joined = use(ButtonGroupContext)
  const resolvedVariant = group?.variant ?? variant ?? "default"
  const resolvedSize = group?.size ?? size ?? "default"
  return (
    <TogglePrimitive
      data-slot="toggle"
      data-variant={resolvedVariant}
      data-size={resolvedSize}
      {...props}
      {...stateStyles(
        (state) => [
          buttonStyles.base,
          variants[resolvedVariant],
          sizes[resolvedSize],
          group?.onWell && resolvedVariant === "default" && styles.onWell,
          group?.segmented && styles.segment,
          state.pressed && !group?.onWell && styles.pressed,
          state.pressed &&
            group?.onWell && [states.key, states.keySurface, styles.pressedKey, states.wellKey],
          props["aria-invalid"] && states.invalidRing,
          state.disabled && states.disabled,
          joined && buttonStyles.group[joined.orientation],
          xstyle,
        ],
        { className, style, hook: "carbon-icons" },
      )}
    />
  )
}

export {
  Toggle,
  ToggleGroupContext,
  toggleStyles,
  type ToggleProps,
  type ToggleVariant,
  type ToggleSize,
}

Update the import paths to match your project setup.

Usage

import { Toggle } from "@/components/ui/toggle"

;<Toggle>Toggle</Toggle>

Use defaultPressed for an initial selection, or pressed and onPressedChange to control it. Give an icon-only toggle an aria-label.

RTL support is not included.

Outline

Use variant="outline" for an outline style.

import { HStack } from "@/components/ui/stack"
import { BoldIcon, ItalicIcon } from "lucide-react"

import { Toggle } from "@/components/ui/toggle"

export default function ToggleOutline() {
  return (
    <HStack wrap="wrap" align="center" gap={2}>
      <Toggle variant="outline" aria-label="Toggle italic">
        <ItalicIcon />
        Italic
      </Toggle>
      <Toggle variant="outline" aria-label="Toggle bold">
        <BoldIcon />
        Bold
      </Toggle>
    </HStack>
  )
}

With text

Place text beside the icon to label the toggle.

import { ItalicIcon } from "lucide-react"

import { Toggle } from "@/components/ui/toggle"

export default function ToggleText() {
  return (
    <Toggle aria-label="Toggle italic">
      <ItalicIcon />
      Italic
    </Toggle>
  )
}

Size

Use size to change the height and minimum width.

import { HStack } from "@/components/ui/stack"
import { Toggle } from "@/components/ui/toggle"

export default function ToggleSizes() {
  return (
    <HStack wrap="wrap" align="center" gap={2}>
      <Toggle variant="outline" aria-label="Toggle small" size="sm">
        Small
      </Toggle>
      <Toggle variant="outline" aria-label="Toggle default" size="default">
        Default
      </Toggle>
      <Toggle variant="outline" aria-label="Toggle large" size="lg">
        Large
      </Toggle>
    </HStack>
  )
}

Disabled

Use disabled to prevent interaction.

import { HStack } from "@/components/ui/stack"
import { Toggle } from "@/components/ui/toggle"

export default function ToggleDisabled() {
  return (
    <HStack wrap="wrap" align="center" gap={2}>
      <Toggle aria-label="Toggle disabled" disabled>
        Disabled
      </Toggle>
      <Toggle variant="outline" aria-label="Toggle disabled outline" disabled>
        Disabled
      </Toggle>
    </HStack>
  )
}

API reference

Toggle accepts Base UI Toggle props, including render, plus xstyle, className and real inline style.

Toggle

PropTypeDefault
variant"default" | "outline""default"
size"sm" | "default" | "lg""default"

A surrounding ToggleGroup supplies variant and size when set on the group.

toggleStyles

KeyWhat it is
baseButton base paired with the shared focus ring
variantsDefault ghost and outline button styles
sizesButton heights and minimum widths
pressedThe flat fill and its text colour
pressedKeyThe raised key a pressed toggle is in a well
onWellRow-highlight hover for a ghost toggle on a well
pressedOnWellThe flat pill face a pressed toggle takes in a well
segmentCorner nested in a segmented group's well
disabledShared disabled opacity and pointer handling

ToggleGroupContext carries group-level variant and size to the toggle, and tells it when it sits in a well: a Toolbar or a segmented Toggle Group.

Data attributes

AttributeOnEffect
data-slot="toggle"buttonTargets the toggle
data-variant, data-sizebuttonMirror the resolved variant and size
data-pressed, data-disabledbuttonBase UI writes the current state