---
title: ToggleButton
description: Toggle a persistent state with independently configured off and on
  button variants.
package: moraine
version: 0.6.0
repository: https://github.com/subframe7536/moraine
---

# ToggleButton

> Toggle a persistent state with independently configured off and on button variants.

Use ToggleButton for persistent on/off actions such as bold formatting or bookmarking. It renders a native button and exposes its state through `aria-pressed`.

## Basic usage

```tsx
import { ToggleButton } from 'moraine'

export function Example() {
  return (
    <ToggleButton variant="ghost" activeVariant="secondary" leading="i-lucide:bold">
      Bold
    </ToggleButton>
  )
}
```

## Anatomy

```text
ToggleButton [component; slot=root; <button>]
├── leading [slot]
├── label [slot]
└── trailing [slot]
```

## Usage

### Off and on variants

`variant` controls the unpressed appearance; `activeVariant` controls the pressed appearance. Both accept all Button variants. The defaults are `ghost` and `secondary`. Changing a variant does not reset the toggle state. The `link` variant is a visual treatment: ToggleButton always renders a button with `type="button"`, so it does not navigate or submit a form.

```tsx
import { ToggleButton } from 'moraine'

export function Variants() {
  return (
    <div class="flex flex-wrap gap-3">
      <ToggleButton>Ghost to secondary</ToggleButton>
      <ToggleButton variant="outline" activeVariant="default">
        Outline to primary
      </ToggleButton>
      <ToggleButton variant="secondary" activeVariant="destructive" defaultPressed>
        Secondary to destructive
      </ToggleButton>
    </div>
  )
}
```

### Controlled and uncontrolled state

Use `defaultPressed` to initialize an uncontrolled button. Pass `pressed` and `onPressedChange` when application state owns the value. Controlled activation reports the requested next value and waits for the caller to update `pressed`.

User `onClick` runs before the toggle. Calling `event.preventDefault()` cancels both the state change and `onPressedChange`.

```tsx
import { Button, ToggleButton } from 'moraine'
import { createSignal } from 'solid-js'

export function Controlled() {
  const [pressed, setPressed] = createSignal(false)
  return (
    <div class="space-y-3">
      <div class="flex flex-wrap gap-3 items-center">
        <ToggleButton pressed={pressed()} onPressedChange={setPressed} leading="i-lucide:pin">
          Pin conversation
        </ToggleButton>
        <Button variant="outline" size="sm" onClick={() => setPressed(false)}>
          Reset
        </Button>
      </div>
      <p class="text-sm text-muted-foreground">
        Conversation: <output aria-live="polite">{pressed() ? 'Pinned' : 'Unpinned'}</output>
      </p>
    </div>
  )
}
```

### Icons and accessible names

Keep the accessible name stable while toggling. Use `aria-label` for icon-only buttons; `aria-pressed` communicates whether their action is enabled. Enter, Space, and clicks activate the native button. Disabled and loading buttons suppress activation.

Children can be a render function receiving reactive `pressed` and `loading` values. This supports state-dependent visual content without replacing the button or remounting its children.

```tsx
import { Icon, ToggleButton } from 'moraine'

export function Icons() {
  return (
    <div class="flex gap-3 items-center">
      <ToggleButton size="icon-md" leading="i-lucide:bold" aria-label="Bold" />
      <ToggleButton size="icon-md" leading="i-lucide:italic" aria-label="Italic" defaultPressed />
      <ToggleButton size="icon-md" aria-label="Bookmark" variant="outline" activeVariant="default">
        {(state) => (
          <Icon
            name="i-lucide:bookmark"
            class={state.pressed ? 'fill-current' : undefined}
            aria-hidden="true"
          />
        )}
      </ToggleButton>
    </div>
  )
}
```

### Async actions

`loadingAuto` follows a Promise returned by `onClick` and blocks repeated activation until it settles. The state changes immediately after an uncancelled click, including when the Promise later rejects; it is not automatically rolled back. Handle request errors in your async handler. To update only after success, use controlled `pressed` and update it after the request completes.

### Theme and groups

Configure `toggleButton.defaultVariants` to choose the default off and on appearances and size. Button's theme still supplies the underlying variant styles; `toggleButton.base`, `classes`, and `styles` add component-specific presentation. Instance styling follows the standard root and slot override rules.

Inside ButtonGroup, ToggleButton inherits the group's size and unpressed variant unless explicitly supplied. Its pressed variant comes from its own configuration. ButtonGroup provides layout, not single or multiple selection behavior.

The root uses `data-slot="toggle-button"` and exposes `data-selected` while pressed. The leading, label, and trailing elements retain the corresponding `button-*` data-slot names.

```tsx
import { ButtonGroup, MoraineProvider, ToggleButton } from 'moraine'
import { defineTheme } from 'moraine/theme'

const theme = defineTheme({
  toggleButton: {
    defaultVariants: { variant: 'outline', activeVariant: 'default', size: 'sm' },
  },
})

export function Theme() {
  return (
    <MoraineProvider theme={theme}>
      <div class="flex flex-wrap gap-4 items-center">
        <ToggleButton leading="i-lucide:bell">Notifications</ToggleButton>
        <ToggleButton leading="i-lucide:mail" activeVariant="secondary" defaultPressed>
          Email updates
        </ToggleButton>
        <ButtonGroup size="sm" variant="ghost" aria-label="Text formatting">
          <ToggleButton leading="i-lucide:bold" aria-label="Bold formatting" />
          <ToggleButton leading="i-lucide:italic" aria-label="Italic formatting" />
        </ButtonGroup>
      </div>
    </MoraineProvider>
  )
}
```

## Attributes

| Attributes | Slot | Description |
| --- | --- | --- |
| `data-selected` | `toggle-button` | Present when the item or tab is selected. |

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| activeVariant | 'default' \| 'secondary' \| 'outline' \| 'ghost' \| 'link' \| 'destructive' \| undefined | 'secondary' | Visual treatment while pressed. |
| defaultPressed | boolean \| undefined | false | Initial uncontrolled toggle state. |
| disabled | boolean \| undefined | — | Disabled state, including for non-button polymorphic roots. |
| leading | IconT.Name \| undefined | — | Leading visual content, usually an icon. |
| loading | boolean \| undefined | false | Controlled loading state. |
| loadingAuto | boolean \| undefined | false | Auto toggles loading while async click handlers are pending. |
| loadingIcon | IconT.Name \| undefined | 'icon-loading' | Optional icon shown when `loading` is active. |
| onPressedChange | ((pressed: boolean) => void) \| undefined | — | Called after an uncancelled activation requests a toggle. |
| pressed | boolean \| undefined | — | Controlled toggle state. |
| size | 'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'icon-xs' \| 'icon-sm' \| 'icon-md' \| 'icon-lg' \| 'icon-xl' \| undefined | 'md' | Button size, including icon-only sizes. |
| trailing | IconT.Name \| undefined | — | Trailing visual content, usually an icon. |
| variant | 'default' \| 'secondary' \| 'outline' \| 'ghost' \| 'link' \| 'destructive' \| undefined | 'ghost' | Visual treatment while unpressed. |
| children | JSX.Element \| ((props: { pressed: boolean; loading: boolean }) => JSX.Element) \| undefined | — | Content or render function receiving reactive toggle and loading states. |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| classes | Classes \| undefined | — | Family slot class defaults for this instance. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |
| styles | Styles \| undefined | — | Family slot style defaults for this instance. |
