Toggle

A toggle is a button that stays pressed until it is pressed again. Use it for something like bold or mute that someone switches on and off.

import { Bold, Italic, VolumeX } from "lucide-react";
import { Toggle } from "@/components/ui/toggle";

export default function ToggleDemo() {
  return (
    <>
      <Toggle size="icon" aria-label="Bold">
        <Bold />
      </Toggle>
      <Toggle size="icon" aria-label="Italic" defaultPressed>
        <Italic />
      </Toggle>
      <Toggle>
        Mute
        <VolumeX />
      </Toggle>
      <Toggle defaultPressed>
        Mute
        <VolumeX />
      </Toggle>
      <Toggle disabled>Disabled</Toggle>
    </>
  );
}

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 { Toggle as BaseToggle } from "@base-ui/react/toggle";
import { cx } from "@/lib/cx";
import { focusRing } from "@/lib/styles";

type Size = "default" | "icon";

// A button that stays pressed. Pressed uses the accent. Hover and pressed are guarded with
// not-data-[disabled], and the scale-down on press is skipped for reduced motion, as in Button.
const base = [
  "inline-flex h-9 cursor-pointer items-center justify-center gap-2 rounded-md border border-text-secondary text-base font-semibold text-text",
  "[&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0",
  "transition-[background-color,border-color,color,scale] duration-150 ease-out",
  focusRing,
  "motion-safe:not-data-[disabled]:active:scale-97",
  "not-data-[disabled]:not-data-[pressed]:hover:bg-text/10",
  "not-data-[disabled]:data-[pressed]:border-accent not-data-[disabled]:data-[pressed]:bg-accent/10 not-data-[disabled]:data-[pressed]:text-accent",
  "not-data-[disabled]:data-[pressed]:hover:bg-accent/20",
  "data-[disabled]:cursor-not-allowed data-[disabled]:border-border data-[disabled]:bg-surface data-[disabled]:text-text-secondary",
].join(" ");

const sizes: Record<Size, string> = {
  default: "px-4",
  icon: "w-9",
};

export function Toggle({
  size = "default",
  className,
  ...props
}: Omit<ComponentProps<typeof BaseToggle>, "className"> & { size?: Size; className?: string }) {
  return <BaseToggle className={cx(base, sizes[size], className)} {...props} />;
}

Props

PropTypeDefaultWhat it does
sizedefault | icondefaulticon makes a square toggle for an icon with no label.
defaultPressedbooleanfalseWhether it starts pressed, when you let it manage its own state.
pressedboolean—Whether it is pressed, when you control it yourself.
onPressedChangefunction—Called with the new value when the toggle is pressed.
disabledbooleanfalseTurns the toggle off.
classNamestring—Extra classes, added after the toggle's own.

Every other prop goes to the Base UI Toggle underneath.

The thinking

A toggle sits between a button and a switch: it looks like a button, and it remembers whether it is on.

  • Built on Base UI: It reports its pressed state to assistive tech, and is operated with the keyboard like any button.
  • Match Button: The same height, radius, text size and press animation as Button, so the two sit side by side in a toolbar.
  • Choose by effect: A toggle suits an action that shows its state on a button face, like Bold. For a setting in a list, use a switch.

Interface design

  • Size: 36px tall with 16px of side padding. The icon toggle is a 36px square. Corners use the md radius, 6px.
  • Off: A 1px outline in the secondary text color and the main text color for the label.
  • Pressed: An accent outline, a 10% accent fill and an accent label, 14.6:1 against the page.
  • Disabled: A surface fill and a quiet label. A disabled toggle that is pressed looks the same as one that isn't.

Interaction

  • Press it: Click it, or press Enter or Space when it has focus. Each press flips it.
  • Hover: An unpressed toggle gets a 10% wash. A pressed one deepens its accent fill to 20%.
  • While you press: The toggle shrinks slightly, to 97%, over 150ms. People who ask for reduced motion don't get the shrink.
  • Focus and disabled: Keyboard focus draws a 2px outline, 2px away. A disabled toggle doesn't react to hover or press.

Empathy

  • Announced: A screen reader says it is a toggle button and whether it is pressed.
  • Keep the name fixed: Don't change the label when it is pressed. Bold stays Bold, and the pressed state tells people it is on. Give icon toggles an aria-label, as the demo does.
  • Color isn't the only signal: The pressed look is a color change, so the pressed state carries the information for anyone who can't see it.
  • Target size: 36px tall clears the 24px minimum of WCAG 2.2 AA.