---
title: Toggle
subtitle: A two-state button that can be on or off.
description: A high-quality, unstyled React toggle component that displays a two-state button that can be on or off.
---

> FineSoft Components documentation. Independent Stencil port; not the official Base UI website.
>
> React API text and `@base-ui/react` examples are upstream references. FineSoft Stencil examples use `finesoft-components`. Do not treat the two packages as interchangeable.

# Toggle

A high-quality, unstyled React toggle component that displays a two-state button that can be on or off.

## Demo

### Tailwind

This example shows how to implement the component using Tailwind CSS.

```tsx
/* docs-comparison.tsx */
import { StencilComparison } from 'docs/src/components/StencilComparison';
import ReactToggle from './react-toggle';
import StencilToggle from './stencil-toggle';
import { StencilToggleView } from './stencil-toggle-view';

export default function ToggleComparison() {
  return <StencilComparison key="tailwind" component="toggle" react={<ReactToggle />} stencil={<StencilToggleView />} clientStencil={<StencilToggle />} />;
}
```

```tsx
/* react-toggle.tsx */
'use client';
import * as React from 'react';
import { Toggle } from '@base-ui/react/toggle';

export default function ExampleToggle() {
  return (
    <Toggle
      aria-label="Favorite"
      className="flex size-8 items-center justify-center border-none rounded-none bg-transparent text-neutral-950 dark:text-white select-none hover:not-data-disabled:bg-neutral-100 dark:hover:not-data-disabled:bg-neutral-800 active:not-data-disabled:bg-neutral-200 dark:active:not-data-disabled:bg-neutral-700 data-pressed:text-neutral-950 dark:data-pressed:text-white focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-neutral-950 dark:focus-visible:outline-white"
      render={(props, state) => {
        if (state.pressed) {
          return (
            <button type="button" {...props}>
              <HeartFilledIcon />
            </button>
          );
        }

        return (
          <button type="button" {...props}>
            <HeartOutlineIcon />
          </button>
        );
      }}
    />
  );
}

function HeartFilledIcon(props: React.ComponentProps<'svg'>) {
  return (
    <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" {...props} style={{ display: 'block', ...props.style }}>
      <path d="M7.99961 13.8667C7.88761 13.8667 7.77561 13.8315 7.68121 13.7611C7.43321 13.5766 1.59961 9.1963 1.59961 5.8667C1.59961 3.80856 3.27481 2.13336 5.33294 2.13336C6.59054 2.13336 7.49934 2.81176 7.99961 3.3131C8.49988 2.81176 9.40868 2.13336 10.6663 2.13336C12.7244 2.13336 14.3996 3.80803 14.3996 5.8667C14.3996 9.1963 8.56601 13.5766 8.31801 13.7616C8.22361 13.8315 8.11161 13.8667 7.99961 13.8667Z" />
    </svg>
  );
}

function HeartOutlineIcon(props: React.ComponentProps<'svg'>) {
  return (
    <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" {...props} style={{ display: 'block', ...props.style }}>
      <path
        fillRule="evenodd"
        clipRule="evenodd"
        d="m7.99961 4.8232-.75505-.75666c-.40333-.40419-1.0559-.86651-1.91162-.86651-1.46903 0-2.66666 1.19764-2.66666 2.66667 0 .5412.24648 1.2356.75339 2.04713.49581.79376 1.17682 1.59861 1.89311 2.33647 1.06989 1.1022 2.1604 1.9962 2.68705 2.4102.52751-.4149 1.61735-1.3085 2.68657-2.4101.7163-.73792 1.3973-1.54278 1.8932-2.33656.5069-.81154.7533-1.50594.7533-2.04714 0-1.46947-1.1975-2.66667-2.6666-2.66667-.85574 0-1.50831.46232-1.91164.86651zm-.01387-1.52394c-.5031-.49988-1.40673-1.1659-2.6528-1.1659-2.05813 0-3.73333 1.6752-3.73333 3.73334 0 3.3296 5.8336 7.7099 6.0816 7.8944a.532.532 0 0 0 .3184.1056c.112 0 .224-.0352.3184-.1051.248-.185 6.08159-4.5653 6.08159-7.8949 0-2.05867-1.6752-3.73334-3.7333-3.73334-1.24617 0-2.14985.66611-2.65293 1.166q-.0069.00686-.0137.01367c.00002-.00003-.00002.00002 0 0-.00459-.0046-.00927-.00914-.01393-.01377"
      />
    </svg>
  );
}
```

```tsx
/* stencil-toggle.tsx */
'use client';
import * as React from 'react';
import { StencilToggleView } from './stencil-toggle-view';

export default function StencilToggle() {
  const [pressed, setPressed] = React.useState(false);
  return <StencilToggleView pressed={pressed} onPressedChange={event => setPressed(event.detail.pressed)} />;
}
```

```tsx
/* stencil-toggle-view.tsx */
import * as React from 'react';
import type { TogglePressedChangeDetail } from 'finesoft-components/define-custom-elements';

export function StencilToggleView({ pressed = false, onPressedChange }: { pressed?: boolean; onPressedChange?: (event: CustomEvent<TogglePressedChangeDetail>) => void }) {
  return (
    <fs-toggle
      aria-label="Favorite"
      className="[&::part(control)]:box-border [&::part(control)]:p-0 [&::part(control)]:[font:inherit] [&::part(control)]:border-current [&::part(control)]:flex [&::part(control)]:size-8 [&::part(control)]:items-center [&::part(control)]:justify-center [&::part(control)]:border-none [&::part(control)]:rounded-none [&::part(control)]:bg-transparent [&::part(control)]:text-neutral-950 dark:[&::part(control)]:text-white [&::part(control)]:select-none not-data-disabled:[&::part(control)]:hover:bg-neutral-100 dark:not-data-disabled:[&::part(control)]:hover:bg-neutral-800 not-data-disabled:[&::part(control)]:active:bg-neutral-200 dark:not-data-disabled:[&::part(control)]:active:bg-neutral-700 data-pressed:[&::part(control)]:text-neutral-950 dark:data-pressed:[&::part(control)]:text-white [&::part(control):focus-visible]:outline-2 [&::part(control):focus-visible]:-outline-offset-1 [&::part(control):focus-visible]:outline-neutral-950 dark:[&::part(control):focus-visible]:outline-white"
      onpressedChange={onPressedChange}
    >
      {pressed ? <HeartFilledIcon /> : <HeartOutlineIcon />}
    </fs-toggle>
  );
}

function HeartFilledIcon(props: React.ComponentProps<'svg'>) {
  return (
    <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" {...props} style={{ display: 'block', ...props.style }}>
      <path d="M7.99961 13.8667C7.88761 13.8667 7.77561 13.8315 7.68121 13.7611C7.43321 13.5766 1.59961 9.1963 1.59961 5.8667C1.59961 3.80856 3.27481 2.13336 5.33294 2.13336C6.59054 2.13336 7.49934 2.81176 7.99961 3.3131C8.49988 2.81176 9.40868 2.13336 10.6663 2.13336C12.7244 2.13336 14.3996 3.80803 14.3996 5.8667C14.3996 9.1963 8.56601 13.5766 8.31801 13.7616C8.22361 13.8315 8.11161 13.8667 7.99961 13.8667Z" />
    </svg>
  );
}

function HeartOutlineIcon(props: React.ComponentProps<'svg'>) {
  return (
    <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" {...props} style={{ display: 'block', ...props.style }}>
      <path
        fillRule="evenodd"
        clipRule="evenodd"
        d="m7.99961 4.8232-.75505-.75666c-.40333-.40419-1.0559-.86651-1.91162-.86651-1.46903 0-2.66666 1.19764-2.66666 2.66667 0 .5412.24648 1.2356.75339 2.04713.49581.79376 1.17682 1.59861 1.89311 2.33647 1.06989 1.1022 2.1604 1.9962 2.68705 2.4102.52751-.4149 1.61735-1.3085 2.68657-2.4101.7163-.73792 1.3973-1.54278 1.8932-2.33656.5069-.81154.7533-1.50594.7533-2.04714 0-1.46947-1.1975-2.66667-2.6666-2.66667-.85574 0-1.50831.46232-1.91164.86651zm-.01387-1.52394c-.5031-.49988-1.40673-1.1659-2.6528-1.1659-2.05813 0-3.73333 1.6752-3.73333 3.73334 0 3.3296 5.8336 7.7099 6.0816 7.8944a.532.532 0 0 0 .3184.1056c.112 0 .224-.0352.3184-.1051.248-.185 6.08159-4.5653 6.08159-7.8949 0-2.05867-1.6752-3.73334-3.7333-3.73334-1.24617 0-2.14985.66611-2.65293 1.166q-.0069.00686-.0137.01367c.00002-.00003-.00002.00002 0 0-.00459-.0046-.00927-.00914-.01393-.01377"
      />
    </svg>
  );
}
```

### CSS Modules

This example shows how to implement the component using CSS Modules.

```css
/* index.module.css */
.Button {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  padding: 0;
  margin: 0;
  border: none;
  border-radius: 0;
  background-color: transparent;
  color: oklch(14.5% 0 0deg);
  -webkit-user-select: none;
  user-select: none;

  @media (prefers-color-scheme: dark) {
    color: white;
  }

  @media (hover: hover) {
    &:hover:not([data-disabled]) {
      background-color: oklch(97% 0 0deg);

      @media (prefers-color-scheme: dark) {
        background-color: oklch(26.9% 0 0deg);
      }
    }
  }

  &:active:not([data-disabled]) {
    background-color: oklch(92.2% 0 0deg);

    @media (prefers-color-scheme: dark) {
      background-color: oklch(37.1% 0 0deg);
    }
  }

  &[data-pressed] {
    color: oklch(14.5% 0 0deg);

    @media (prefers-color-scheme: dark) {
      color: white;
    }
  }

  &:focus-visible {
    outline: 2px solid oklch(14.5% 0 0deg);
    outline-offset: -1px;

    @media (prefers-color-scheme: dark) {
      outline-color: white;
    }
  }
}
```

```tsx
/* docs-comparison.tsx */
import { StencilComparison } from 'docs/src/components/StencilComparison';
import ReactToggle from './react-toggle';
import StencilToggle from './stencil-toggle';
import { StencilToggleView } from './stencil-toggle-view';

export default function ToggleComparison() {
  return <StencilComparison key="css-modules" component="toggle" react={<ReactToggle />} stencil={<StencilToggleView />} clientStencil={<StencilToggle />} />;
}
```

```tsx
/* react-toggle.tsx */
'use client';
import * as React from 'react';
import { Toggle } from '@base-ui/react/toggle';
import styles from './index.module.css';

export default function ExampleToggle() {
  return (
    <div className={styles.Panel}>
      <Toggle
        aria-label="Favorite"
        className={styles.Button}
        render={(props, state) => {
          if (state.pressed) {
            return (
              <button type="button" {...props}>
                <HeartFilledIcon />
              </button>
            );
          }

          return (
            <button type="button" {...props}>
              <HeartOutlineIcon />
            </button>
          );
        }}
      />
    </div>
  );
}

function HeartFilledIcon(props: React.ComponentProps<'svg'>) {
  return (
    <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" {...props} style={{ display: 'block', ...props.style }}>
      <path d="M7.99961 13.8667C7.88761 13.8667 7.77561 13.8315 7.68121 13.7611C7.43321 13.5766 1.59961 9.1963 1.59961 5.8667C1.59961 3.80856 3.27481 2.13336 5.33294 2.13336C6.59054 2.13336 7.49934 2.81176 7.99961 3.3131C8.49988 2.81176 9.40868 2.13336 10.6663 2.13336C12.7244 2.13336 14.3996 3.80803 14.3996 5.8667C14.3996 9.1963 8.56601 13.5766 8.31801 13.7616C8.22361 13.8315 8.11161 13.8667 7.99961 13.8667Z" />
    </svg>
  );
}

function HeartOutlineIcon(props: React.ComponentProps<'svg'>) {
  return (
    <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" {...props} style={{ display: 'block', ...props.style }}>
      <path
        fillRule="evenodd"
        clipRule="evenodd"
        d="m7.99961 4.8232-.75505-.75666c-.40333-.40419-1.0559-.86651-1.91162-.86651-1.46903 0-2.66666 1.19764-2.66666 2.66667 0 .5412.24648 1.2356.75339 2.04713.49581.79376 1.17682 1.59861 1.89311 2.33647 1.06989 1.1022 2.1604 1.9962 2.68705 2.4102.52751-.4149 1.61735-1.3085 2.68657-2.4101.7163-.73792 1.3973-1.54278 1.8932-2.33656.5069-.81154.7533-1.50594.7533-2.04714 0-1.46947-1.1975-2.66667-2.6666-2.66667-.85574 0-1.50831.46232-1.91164.86651zm-.01387-1.52394c-.5031-.49988-1.40673-1.1659-2.6528-1.1659-2.05813 0-3.73333 1.6752-3.73333 3.73334 0 3.3296 5.8336 7.7099 6.0816 7.8944a.532.532 0 0 0 .3184.1056c.112 0 .224-.0352.3184-.1051.248-.185 6.08159-4.5653 6.08159-7.8949 0-2.05867-1.6752-3.73334-3.7333-3.73334-1.24617 0-2.14985.66611-2.65293 1.166q-.0069.00686-.0137.01367c.00002-.00003-.00002.00002 0 0-.00459-.0046-.00927-.00914-.01393-.01377"
      />
    </svg>
  );
}
```

```tsx
/* stencil-toggle.tsx */
'use client';
import * as React from 'react';
import { StencilToggleView } from './stencil-toggle-view';

export default function StencilToggle() {
  const [pressed, setPressed] = React.useState(false);
  return <StencilToggleView pressed={pressed} onPressedChange={event => setPressed(event.detail.pressed)} />;
}
```

```tsx
/* stencil-toggle-view.tsx */
import * as React from 'react';
import type { TogglePressedChangeDetail } from 'finesoft-components/define-custom-elements';

export function StencilToggleView({ pressed = false, onPressedChange }: { pressed?: boolean; onPressedChange?: (event: CustomEvent<TogglePressedChangeDetail>) => void }) {
  return (
    <div className={undefined}>
      <fs-toggle aria-label="Favorite" onpressedChange={onPressedChange}>
        {pressed ? <HeartFilledIcon /> : <HeartOutlineIcon />}
      </fs-toggle>
    </div>
  );
}

function HeartFilledIcon(props: React.ComponentProps<'svg'>) {
  return (
    <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" {...props} style={{ display: 'block', ...props.style }}>
      <path d="M7.99961 13.8667C7.88761 13.8667 7.77561 13.8315 7.68121 13.7611C7.43321 13.5766 1.59961 9.1963 1.59961 5.8667C1.59961 3.80856 3.27481 2.13336 5.33294 2.13336C6.59054 2.13336 7.49934 2.81176 7.99961 3.3131C8.49988 2.81176 9.40868 2.13336 10.6663 2.13336C12.7244 2.13336 14.3996 3.80803 14.3996 5.8667C14.3996 9.1963 8.56601 13.5766 8.31801 13.7616C8.22361 13.8315 8.11161 13.8667 7.99961 13.8667Z" />
    </svg>
  );
}

function HeartOutlineIcon(props: React.ComponentProps<'svg'>) {
  return (
    <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" {...props} style={{ display: 'block', ...props.style }}>
      <path
        fillRule="evenodd"
        clipRule="evenodd"
        d="m7.99961 4.8232-.75505-.75666c-.40333-.40419-1.0559-.86651-1.91162-.86651-1.46903 0-2.66666 1.19764-2.66666 2.66667 0 .5412.24648 1.2356.75339 2.04713.49581.79376 1.17682 1.59861 1.89311 2.33647 1.06989 1.1022 2.1604 1.9962 2.68705 2.4102.52751-.4149 1.61735-1.3085 2.68657-2.4101.7163-.73792 1.3973-1.54278 1.8932-2.33656.5069-.81154.7533-1.50594.7533-2.04714 0-1.46947-1.1975-2.66667-2.6666-2.66667-.85574 0-1.50831.46232-1.91164.86651zm-.01387-1.52394c-.5031-.49988-1.40673-1.1659-2.6528-1.1659-2.05813 0-3.73333 1.6752-3.73333 3.73334 0 3.3296 5.8336 7.7099 6.0816 7.8944a.532.532 0 0 0 .3184.1056c.112 0 .224-.0352.3184-.1051.248-.185 6.08159-4.5653 6.08159-7.8949 0-2.05867-1.6752-3.73334-3.7333-3.73334-1.24617 0-2.14985.66611-2.65293 1.166q-.0069.00686-.0137.01367c.00002-.00003-.00002.00002 0 0-.00459-.0046-.00927-.00914-.01393-.01377"
      />
    </svg>
  );
}
```

The Stencil example uses `fs-toggle`. The component owns its uncontrolled pressed state;
the visible `pressedChange` listener only chooses the icon, replacing React's render callback.
The shared `stencil-toggle-view.tsx` is used by both the live example and its server-rendered preview.

`native-button="false"` uses a span with button semantics. React's `render` callback is not a
Web Component property: use slotted content and the public change event instead. `type` and
`form` overrides are ignored, and `value` is not submitted as a native button value, matching Base UI.
Inside `fs-toggle-group`, selection, disabled inheritance, and the roving tab stop are managed
by the migrated group. Grouped Space activation occurs on keydown, matching Base UI's composite
behavior. Inside `fs-toolbar-root`, the toolbar supplies the outer roving tab stop.
Use `fs-toolbar-button asChild` to compose a focusable-when-disabled toolbar toggle.

## Anatomy

Import the component and use it as a single part:

```jsx title="Anatomy"
import { Toggle } from '@base-ui/react/toggle';

<Toggle />;
```

## API reference

### Toggle

A two-state button that can be on or off.
Renders a `<button>` element.

**Toggle Props:**

| Prop            | Type                                                                                 | Default | Description                                                                                                                                                                                   |
| :-------------- | :----------------------------------------------------------------------------------- | :------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| value           | `string`                                                                             | -       | A unique string that identifies the toggle when used&#xA;inside a toggle group.                                                                                                               |
| defaultPressed  | `boolean`                                                                            | `false` | Whether the toggle button is currently pressed.&#xA;This is the uncontrolled counterpart of `pressed`.                                                                                        |
| pressed         | `boolean`                                                                            | -       | Whether the toggle button is currently pressed.&#xA;This is the controlled counterpart of `defaultPressed`.                                                                                   |
| onPressedChange | `((pressed: boolean, eventDetails: Toggle.ChangeEventDetails) => void)`              | -       | Callback fired when the pressed state is changed.                                                                                                                                             |
| nativeButton    | `boolean`                                                                            | `true`  | Whether the component renders a native `<button>` element when replacing it&#xA;via the `render` prop.&#xA;Set to `false` if the rendered element is not a button (for example, `<div>`).     |
| disabled        | `boolean`                                                                            | `false` | Whether the component should ignore user interaction.                                                                                                                                         |
| className       | `string \| ((state: Toggle.State) => string \| undefined)`                           | -       | CSS class applied to the element, or a function that&#xA;returns a class based on the component's state.                                                                                      |
| style           | `React.CSSProperties \| ((state: Toggle.State) => React.CSSProperties \| undefined)` | -       | Style applied to the element, or a function that&#xA;returns a style object based on the component's state.                                                                                   |
| render          | `ReactElement \| ((props: HTMLProps, state: Toggle.State) => ReactElement)`          | -       | Allows you to replace the component's HTML element&#xA;with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |

**Toggle Data Attributes:**

| Attribute     | Type | Description                                 |
| :------------ | :--- | :------------------------------------------ |
| data-pressed  | -    | Present when the toggle button is pressed.  |
| data-disabled | -    | Present when the toggle button is disabled. |

### Toggle.Props

Re-export of [Toggle](/react/components/toggle.md) props.

### Toggle.State

```typescript
type ToggleState = {
  /** Whether the toggle is currently pressed. */
  pressed: boolean;
  /** Whether the toggle should ignore user interaction. */
  disabled: boolean;
};
```

### Toggle.ChangeEventReason

```typescript
type ToggleChangeEventReason = 'none';
```

### Toggle.ChangeEventDetails

```typescript
type ToggleChangeEventDetails = {
  /** The reason for the event. */
  reason: 'none';
  /** The native event associated with the custom event. */
  event: Event;
  /** Cancels Base UI from handling the event. */
  cancel: () => void;
  /** Allows the event to propagate in cases where Base UI will stop the propagation. */
  allowPropagation: () => void;
  /** Indicates whether the event has been canceled. */
  isCanceled: boolean;
  /** Indicates whether the event is allowed to propagate. */
  isPropagationAllowed: boolean;
  /** The element that triggered the event, if applicable. */
  trigger: Element | undefined;
};
```

## Canonical Types

Maps `Canonical`: `Alias` — Use Canonical when its namespace is already imported; otherwise use Alias.

- `Toggle.State`: `ToggleState`
- `Toggle.Props`: `ToggleProps`
- `Toggle.ChangeEventReason`: `ToggleChangeEventReason`
- `Toggle.ChangeEventDetails`: `ToggleChangeEventDetails`
