---
name: Toggle
type: ATM
section: actions
why: Two-state button carrying aria-pressed - activation is native.
when: A single on/off action: mute, bold, favorite.
where: dist/components/toggle/toggle.css + dist/components/toggle/toggle.js
supportedStates: default, pressed
---

# Pattern: Toggle

## Native basis
`<button>` element with `aria-pressed`. The browser provides click and keyboard
(Enter/Space) handling automatically. No extra JavaScript needed for basic usage.

A two-state button that can be either on or off. Common for toolbar formatting
buttons (bold, italic, underline) or feature toggles.

---

## Native Web APIs
- [`<button>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button) - native clickable element with built-in keyboard handling
- [`aria-pressed`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-pressed) - communicates toggle on/off state to assistive technology
- [`color-mix()`](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/color-mix) - derived hover color for pressed state
- [`prefers-reduced-motion`](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion) - suppresses color/background transitions
- [`prefers-contrast`](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-contrast) - adds visible borders in high-contrast mode
- [`forced-colors`](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/forced-colors) - maps pressed state to system Highlight in Windows High Contrast Mode

---

## Structure

```html
<button class="toggle" aria-pressed="false" aria-label="Toggle bold">
  <svg aria-hidden="true" width="16" height="16" viewBox="0 0 24 24" fill="none"
       stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
    <path d="M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8"/>
  </svg>
</button>
```

### With text
```html
<button class="toggle" aria-pressed="false">
  <svg aria-hidden="true" width="16" height="16" ...>...</svg>
  Italic
</button>
```

---

## Variants

| `data-variant` | Purpose                              |
|----------------|--------------------------------------|
| *(default)*    | Transparent background, subtle hover |
| `outline`      | Border, transparent background       |

```html
<button class="toggle" aria-pressed="false">Default</button>
<button class="toggle" data-variant="outline" aria-pressed="false">Outline</button>
```

---

## Sizes

| `data-size`  | Height    | Padding        | Min-width   |
|-------------|-----------|----------------|-------------|
| `xs`        | `1.75rem` | `0 0.25rem`    | `1.75rem`   |
| `sm`        | `2rem`    | `0 0.375rem`   | `2rem`      |
| `md`        | `2.25rem` | `0 0.5rem`     | `2.25rem`   |
| *(default)* | `2.25rem` | `0 0.5rem`     | `2.25rem`   |
| `lg`        | `2.5rem`  | `0 0.625rem`   | `2.5rem`    |
| `xl`        | `3rem`    | `0 0.75rem`    | `3rem`      |

```html
<button class="toggle" data-size="sm" aria-pressed="false" aria-label="Bold">
  <svg aria-hidden="true" ...>...</svg>
</button>
```

---

## ARIA

| Attribute         | Element    | Value               |
|--------------------|-----------|----------------------|
| `aria-pressed`     | `<button>` | `"true"` or `"false"` - toggled on click |
| `aria-label`       | `<button>` | Required for icon-only toggles |

---

## States

Declared states: `default` (unpressed - the authored `aria-pressed` value is
restored) · `pressed` (`aria-pressed="true"`).

```js
const toggle = df$('#my-toggle').get(0);
toggle.api.setState('pressed');
toggle.api.getState(); // { name: 'pressed', config: {}, model: { … } }
toggle.api.render();   // the authored button with aria-pressed="true" - markup of the state
```

`render(state)` reproduces the markup from state: the authored markup (its
model, snapshotted at init, byte for byte) with the state's `aria-pressed` applied - AGENTS.md "State
API" → render.

The api is bound per button; the registry global is
`df$.shadcn.toggleApi` / `df$.shadcn.toggleStates`.

## API

<!-- generated from the source by `bun run api-docs` - edit the JSDoc and the types in the .ts, not this section -->

### States

<code>type ToggleState = 'default' | 'pressed'</code> - `setState(name, config)` takes the config of the state it names (`ToggleStateConfigs[name]`).

| State | Description |
|---|---|
| `default` | Not pressed (aria-pressed="false"). No config. |
| `pressed` | Pressed (aria-pressed="true"). No config. |

### Every element

| Member | Description |
|---|---|
| <code>el.api.setState&lt;S extends ToggleState&gt;(name: S, config?: ToggleStateConfigs[S]): unknown</code> | Enter a state: the DOM work runs (also when it is the current state), the store records it. <table><tr><th>Argument</th><th>Type</th><th>Description</th></tr><tr><td><code>name</code></td><td><code>S</code></td><td>a declared state (an unknown name throws)</td></tr><tr><td><code>config?</code></td><td><code>ToggleStateConfigs[S]</code></td><td>that state's config (merged into the stored one when the component merges)</td></tr></table> <b>Returns</b> <code>unknown</code> - what the state's DOM work returned - a Promise for an async state (or await settled()) |
| <code>el.api.getState(): { name: ToggleState; config: ToggleStateConfigs[ToggleState]; model?: ElementModel }</code> | The state the element shows now - read back from the DOM, so it includes what the user changed. <b>Returns</b> <code>{ name: ToggleState; config: ToggleStateConfigs[ToggleState]; model?: ElementModel }</code> - the state's name, its config and the authored markup model render() starts from |
| <code>el.api.render(state?: { name: ToggleState; config: ToggleStateConfigs[ToggleState]; model?: ElementModel }): string</code> | The element's markup in a state - the authored markup with that state applied; a pure function of the state. <table><tr><th>Argument</th><th>Type</th><th>Description</th></tr><tr><td><code>state?</code></td><td><code>{ name: ToggleState; config: ToggleStateConfigs[ToggleState]; model?: ElementModel }</code></td><td>a state as getState() returns it (default: the current one)</td></tr></table> <b>Returns</b> <code>string</code> - the element's outer HTML in that state |
| <code>el.api.settled(): Promise&lt;void&gt;</code> | Wait for the last state's DOM work (async states: a diagram rendering, a chart mounting). <b>Returns</b> <code>Promise&lt;void&gt;</code> - resolves when nothing is pending |
| <code>el.store: Store&lt;{ name: ToggleState; config: ToggleStateConfigs[ToggleState] }&gt;</code> | A defuss-store store of the element's state - subscribe to follow every change (also the user's), set it to drive the component. |

### Registry

| Member | Description |
|---|---|
| <code>df$.shadcn.toggleApi.setState&lt;S extends ToggleState&gt;(el: HTMLElement, name: S, config?: ToggleStateConfigs[S]): unknown</code> | Enter a state: the DOM work runs (also when it is the current state), the store records it. <table><tr><th>Argument</th><th>Type</th><th>Description</th></tr><tr><td><code>el</code></td><td><code>HTMLElement</code></td><td>the component's element</td></tr><tr><td><code>name</code></td><td><code>S</code></td><td>a declared state (an unknown name throws)</td></tr><tr><td><code>config?</code></td><td><code>ToggleStateConfigs[S]</code></td><td>that state's config (merged into the stored one when the component merges)</td></tr></table> <b>Returns</b> <code>unknown</code> - what the state's DOM work returned - a Promise for an async state (await it, or el.api.settled()) |
| <code>df$.shadcn.toggleApi.getState(el: HTMLElement): { name: ToggleState; config: ToggleStateConfigs[ToggleState]; model?: ElementModel }</code> | The state the element shows now - read back from the DOM, so it includes what the user changed. <table><tr><th>Argument</th><th>Type</th><th>Description</th></tr><tr><td><code>el</code></td><td><code>HTMLElement</code></td><td>the component's element</td></tr></table> <b>Returns</b> <code>{ name: ToggleState; config: ToggleStateConfigs[ToggleState]; model?: ElementModel }</code> - the state's name, its config and the authored markup model render() starts from |
| <code>df$.shadcn.toggleApi.render(state: { name: ToggleState; config: ToggleStateConfigs[ToggleState]; model?: ElementModel }): string</code> | The element's markup in a state - the authored markup with that state applied; a pure function of the state. <table><tr><th>Argument</th><th>Type</th><th>Description</th></tr><tr><td><code>state</code></td><td><code>{ name: ToggleState; config: ToggleStateConfigs[ToggleState]; model?: ElementModel }</code></td><td>a state as getState() returns it (with its model)</td></tr></table> <b>Returns</b> <code>string</code> - the element's outer HTML in that state |
| <code>df$.shadcn.toggleApi.store(el: HTMLElement): Store&lt;{ name: ToggleState; config: ToggleStateConfigs[ToggleState] }&gt;</code> | The element's store (bindComponent made it). <table><tr><th>Argument</th><th>Type</th><th>Description</th></tr><tr><td><code>el</code></td><td><code>HTMLElement</code></td><td>the component's element</td></tr></table> <b>Returns</b> <code>Store&lt;{ name: ToggleState; config: ToggleStateConfigs[ToggleState] }&gt;</code> - a defuss-store store of { name, config } - subscribe to follow every change, set it to drive the component |
| <code>df$.shadcn.toggleApi.commit&lt;S extends ToggleState&gt;(el: HTMLElement, name: S, config?: ToggleStateConfigs[S]): void</code> | Record a state the element reached on its own (no DOM work) - for a component's own handlers. <table><tr><th>Argument</th><th>Type</th><th>Description</th></tr><tr><td><code>el</code></td><td><code>HTMLElement</code></td><td>the component's element</td></tr><tr><td><code>name</code></td><td><code>S</code></td><td>the state it is in</td></tr><tr><td><code>config?</code></td><td><code>ToggleStateConfigs[S]</code></td><td>its config</td></tr></table> |
| <code>df$.shadcn.toggleStates: ToggleState[]</code> | The declared states, 'default' first: <code>default</code>, <code>pressed</code>. |

## Notes

- The toggle is just a button with `aria-pressed` - no custom elements needed.
- Icon-only toggles must have `aria-label` for screen readers.
- For toggle groups (e.g., text alignment), wrap in a container with `role="group"` and `aria-label`.
- The pressed state uses `--accent` / `--accent-foreground` to match shadcn conventions.
