# Toggle

> A button that stays on or off, with a fill that settles in when pressed, a clear hover-to-on step and icons that can fill with the state.

Docs: https://hextaui.com/docs/toggle
Markdown: https://hextaui.com/docs/toggle.md

```tsx title="components/examples/toggle/demo.tsx"
import { IconBold, IconItalic, IconUnderline } from "@tabler/icons-react"

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

export function ToggleDemo() {
  return (
    <div className="flex items-center gap-1">
      <Toggle aria-label="Bold" defaultPressed>
        <IconBold />
      </Toggle>
      <Toggle aria-label="Italic">
        <IconItalic />
      </Toggle>
      <Toggle aria-label="Underline">
        <IconUnderline />
      </Toggle>
    </div>
  )
}
```

## Installation

### CLI

```bash
npx shadcn@latest add https://hextaui.com/r/toggle.json
```

This adds the component, the HextaUI theme tokens and any HextaUI components it depends on.

### Manual

Add the HextaUI theme tokens (https://hextaui.com/docs/installation#theme) to your global CSS if you haven't yet, then install the dependencies.

```bash
pnpm add @base-ui/react class-variance-authority cn
```

Copy and paste the following code into your project.

```tsx title="components/ui/toggle.tsx"
"use client"

import * as React from "react"
import { Toggle as TogglePrimitive } from "@base-ui/react/toggle"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"

type ClassName<State> =
  string | ((state: State) => string | undefined) | undefined

function mergeClassName<State>(base: string, className: ClassName<State>) {
  return typeof className === "function"
    ? (state: State) => cn(base, className(state))
    : cn(base, className)
}

const toggleVariants = cva(
  "group/toggle relative isolate inline-flex shrink-0 cursor-pointer items-center justify-center gap-1.5 rounded-md bg-clip-padding text-sm font-medium whitespace-nowrap text-muted-foreground inset-ring-(length:--hairline) inset-ring-transparent transition-[color,background-color,box-shadow,opacity,translate,scale] duration-200 ease-out-quint outline-none select-none before:pointer-events-none before:absolute before:inset-(--hairline) before:-z-1 before:rounded-[inherit] before:bg-foreground/8 before:opacity-0 before:transition-[opacity,scale] before:duration-200 before:ease-out-quint hover:text-foreground focus-visible:z-10 focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:inset-ring-ring focus-visible:outline-hidden active:duration-100 aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:inset-ring-destructive data-pressed:text-foreground data-pressed:before:opacity-100 motion-safe:before:scale-[0.88] motion-safe:active:not-data-disabled:translate-y-px motion-safe:active:not-data-disabled:scale-[0.97] motion-safe:data-pressed:before:scale-100 dark:before:bg-foreground/12 dark:aria-invalid:ring-destructive/40 dark:aria-invalid:inset-ring-destructive/50 forced-colors:border forced-colors:before:hidden forced-colors:data-pressed:outline-2 forced-colors:data-pressed:-outline-offset-2 forced-colors:data-pressed:outline-solid pointer-coarse:after:absolute pointer-coarse:in-data-[slot=toggle-group]:after:hidden data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:transition-[color,fill] [&_svg]:duration-200 [&_svg]:ease-out-quint [&_svg:not([class*='size-'])]:size-4",
  {
    variants: {
      variant: {
        default: "bg-transparent hover:bg-muted dark:hover:bg-muted/50",
        outline:
          "bg-background inset-ring-border hover:bg-muted dark:bg-input/30 dark:inset-ring-input dark:hover:bg-input/50",
      },
      size: {
        default: "h-9 min-w-9 px-2 pointer-coarse:after:-inset-1",
        sm: "h-8 min-w-8 rounded-[min(var(--radius-md),10px)] px-1.5 pointer-coarse:after:-inset-1.5",
        lg: "h-10 min-w-10 px-2.5 pointer-coarse:after:-inset-0.5",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

type ToggleProps<Value extends string = string> = TogglePrimitive.Props<Value> &
  VariantProps<typeof toggleVariants>

function Toggle<Value extends string = string>({
  className,
  variant = "default",
  size = "default",
  ...props
}: ToggleProps<Value>) {
  return (
    <TogglePrimitive
      data-variant={variant}
      data-size={size}
      className={mergeClassName(toggleVariants({ variant, size }), className)}
      {...props}
      data-slot="toggle"
    />
  )
}

export { Toggle, toggleVariants }
export type { ToggleProps }
```

Update the import paths to match your project setup.

## Usage

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

```tsx
<Toggle aria-label="Bold">
  <IconBold />
</Toggle>
```

Hovering an off toggle shows a light tint; turning it on settles a stronger fill into place and brings the label to full contrast, so on never looks like hover.

## Examples

### Outline

`variant="outline"` adds a border and a surface, for toggles that sit on their own.

```tsx title="components/examples/toggle/outline.tsx"
import { IconBookmark } from "@tabler/icons-react"

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

export function ToggleOutline() {
  return (
    <div className="flex items-center gap-2">
      <Toggle variant="outline" aria-label="Bookmark">
        <IconBookmark />
      </Toggle>
      <Toggle variant="outline">
        <IconBookmark />
        Bookmark
      </Toggle>
    </div>
  )
}
```

### With text

Icons and labels sit side by side. Toggles with visible text don't need an aria-label.

```tsx title="components/examples/toggle/text.tsx"
import { IconItalic, IconTextWrap } from "@tabler/icons-react"

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

export function ToggleText() {
  return (
    <div className="flex flex-wrap items-center gap-2">
      <Toggle>
        <IconItalic />
        Italic
      </Toggle>
      <Toggle variant="outline" defaultPressed>
        <IconTextWrap />
        Wrap lines
      </Toggle>
      <Toggle variant="outline">Show hidden files</Toggle>
    </div>
  )
}
```

### Filled when on

Add `group-data-pressed/toggle:fill-current` to an outline icon and it fills in as the toggle turns on.

```tsx title="components/examples/toggle/filled-icon.tsx"
import { IconBookmark, IconHeart, IconStar } from "@tabler/icons-react"

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

export function ToggleFilledIcon() {
  return (
    <div className="flex items-center gap-1">
      <Toggle aria-label="Like" defaultPressed>
        <IconHeart className="group-data-pressed/toggle:fill-current" />
      </Toggle>
      <Toggle aria-label="Star">
        <IconStar className="group-data-pressed/toggle:fill-current" />
      </Toggle>
      <Toggle aria-label="Save">
        <IconBookmark className="group-data-pressed/toggle:fill-current" />
      </Toggle>
    </div>
  )
}
```

### Sizes

`sm`, `default` and `lg`. Icon-only toggles stay square.

```tsx title="components/examples/toggle/sizes.tsx"
import { IconBold } from "@tabler/icons-react"

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

export function ToggleSizes() {
  return (
    <div className="flex flex-col gap-4">
      <div className="flex items-center gap-2">
        <Toggle size="sm" aria-label="Bold, small">
          <IconBold />
        </Toggle>
        <Toggle aria-label="Bold">
          <IconBold />
        </Toggle>
        <Toggle size="lg" aria-label="Bold, large">
          <IconBold />
        </Toggle>
      </div>
      <div className="flex items-center gap-2">
        <Toggle size="sm" variant="outline">
          Small
        </Toggle>
        <Toggle variant="outline">Default</Toggle>
        <Toggle size="lg" variant="outline">
          Large
        </Toggle>
      </div>
    </div>
  )
}
```

### Disabled

A disabled toggle keeps showing whether it's on, but can't be pressed or focused.

```tsx title="components/examples/toggle/disabled.tsx"
import { IconBold, IconUnderline } from "@tabler/icons-react"

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

export function ToggleDisabled() {
  return (
    <div className="flex items-center gap-2">
      <Toggle disabled aria-label="Bold">
        <IconBold />
      </Toggle>
      <Toggle disabled defaultPressed aria-label="Underline">
        <IconUnderline />
      </Toggle>
      <Toggle disabled variant="outline">
        Read-only mode
      </Toggle>
    </div>
  )
}
```

### Controlled

Pass `pressed` and `onPressedChange`. The label stays the same while the icon follows the state.

```tsx title="components/examples/toggle/controlled.tsx"
"use client"

import * as React from "react"
import { IconMicrophone, IconMicrophoneOff } from "@tabler/icons-react"

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

export function ToggleControlled() {
  const [muted, setMuted] = React.useState(false)

  return (
    <div className="flex items-center gap-3 text-sm">
      <Toggle
        variant="outline"
        aria-label="Mute microphone"
        pressed={muted}
        onPressedChange={setMuted}
      >
        {muted ? <IconMicrophoneOff /> : <IconMicrophone />}
      </Toggle>
      <span className="text-muted-foreground">
        Microphone is {muted ? "muted" : "on"}
      </span>
    </div>
  )
}
```

### Right to left

Icons lead the label from the right.

```tsx title="components/examples/toggle/rtl.tsx"
import { IconAlignRight, IconBold, IconItalic } from "@tabler/icons-react"

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

export function ToggleRtl() {
  return (
    <div dir="rtl" className="flex items-center gap-2">
      <Toggle variant="outline" defaultPressed>
        <IconBold />
        عريض
      </Toggle>
      <Toggle variant="outline">
        <IconItalic />
        مائل
      </Toggle>
      <Toggle variant="outline" aria-label="محاذاة لليمين">
        <IconAlignRight />
      </Toggle>
    </div>
  )
}
```

## Keyboard

| Key | Action |
| --- | --- |
| `Space` `Enter` | Turns the focused toggle on or off. |
| `Tab` | Moves focus to the next toggle. |

## Accessibility

- The toggle is a `button` with `aria-pressed`, so screen readers announce whether it's on.
- Give icon-only toggles an `aria-label` that names the action ("Mute microphone") and keep it the same in both states. Changing it as well as the pressed state reads as a double negative.
- On touch screens the hit area grows to at least 44px, except inside a toggle group where neighbours would overlap.
- With reduced motion, nothing scales; the fill only fades.

## API reference

### Toggle

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `"default" \| "outline"` | `"default"` |  |
| `size` | `"default" \| "sm" \| "lg"` | `"default"` |  |
| `pressed` | `boolean` | – |  |
| `defaultPressed` | `boolean` | `false` |  |
| `onPressedChange` | `(pressed, details) => void` | – |  |
| `disabled` | `boolean` | `false` |  |
| `value` | `string` | – | Identifies the toggle inside a toggle group. |
| `className` | `string \| (state) => string` | – |  |
| `nativeButton` | `boolean` | `true` |  |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<button>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="toggle"` | The button, with data-variant and data-size. |
| `data-pressed` | Present when the toggle is on. |
| `data-disabled` | Present when the toggle is disabled. |

## Notes for AI assistants

- Install a component with the shadcn CLI: `npx shadcn@latest add https://hextaui.com/r/<name>.json`. It adds the source, the HextaUI theme tokens and any HextaUI components it depends on. `https://hextaui.com/r/all.json` installs every component.
- The code is then owned by the project, like shadcn/ui. There is no HextaUI npm package. HextaUI is MIT licensed and free for personal and commercial use.
- Behavior and accessibility come from Base UI (`@base-ui/react`). Compose with the `render` prop, not `asChild`.
- Styling uses Tailwind CSS v4 with theme tokens. Merge classes with `cn` from the `cn` package.
- Icons come from `@tabler/icons-react`.
- Import components from `@/components/ui/<name>`, hooks from `@/hooks/<name>` and utilities from `@/lib/<name>`.

Every HextaUI doc: https://hextaui.com/llms.txt
