---
title: Toggle
description: A two-state button that can be either on or off.
order: 21
tags: [primitives]
---

# Toggle

A pressable button that toggles between pressed and unpressed states. Built on `role="button"` with `aria-pressed`, it emits change events and supports both button and non-button root elements.

```ts
import { Toggle } from "@areia/slots";

document.querySelector("#root")!.innerHTML = `
  <button data-slot="toggle" class="win95-button data-[state=on]:border-t-black data-[state=on]:border-l-black data-[state=on]:border-r-white data-[state=on]:border-b-white">Bold</button>
`;

const root = document.querySelector<HTMLElement>(
  '[data-slot="toggle"]',
)!;
const controller = Toggle.createToggle(root, {
  defaultPressed: false,
});
```

## Import

```ts
import { Toggle } from "@areia/slots";
```

## Usage

```ts
import { Toggle } from "@areia/slots";

// Auto-bind a single root
const root = document.querySelector('[data-slot="toggle"]');
const controller = Toggle.createToggle(root, {
  defaultPressed: false,
  onPressedChange: (pressed) =>
    console.log("pressed:", pressed),
});

// Listen for state changes
root.addEventListener("toggle:change", (e) => {
  console.log(e.detail.pressed);
});

// Auto-bind all unbound roots in scope
const controllers = Toggle.create();
```

## Expected Markup

The toggle is a single-element primitive — there are no sub-slots. It can be a native `<button>` or any other focusable element.

```html
<!-- Native button (recommended) -->
<button data-slot="toggle">Bold</button>

<!-- Non-button element -->
<div data-slot="toggle" tabindex="0" role="button">Italic</div>
```

When the root is a native `<button>`, the controller sets `type="button"` (if not already set) to prevent accidental form submission. Button elements handle click and keyboard events natively.

### Data Slots

| Slot     | Required | Description                                      |
| -------- | -------- | ------------------------------------------------ |
| `toggle` | Yes      | Root element. Receives `aria-pressed` and state. |

### Generated Attributes

| Attribute       | Element | Description                                                                               |
| --------------- | ------- | ----------------------------------------------------------------------------------------- |
| `data-state`    | Root    | `"on"` when pressed, `"off"` when not pressed.                                            |
| `aria-pressed`  | Root    | `"true"` or `"false"`.                                                                    |
| `aria-disabled` | Root    | `"true"` when disabled (on all element types).                                            |
| `disabled`      | Root    | Native attribute set on `<button>` elements when disabled.                                |
| `type`          | Root    | Set to `"button"` on `<button>` elements to prevent form submission (unless already set). |
| `role`          | Root    | Set to `"button"` when the root is not already a `<button>`.                              |

## Events

### Outbound

| Event           | Detail                 | Description                                       |
| --------------- | ---------------------- | ------------------------------------------------- |
| `toggle:change` | `{ pressed: boolean }` | Fired on the root when the pressed state changes. |

### Inbound

| Event        | Detail                                                                                           | Description                                                                            |
| ------------ | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- |
| `toggle:set` | `{ value: boolean }` (preferred), `boolean` (deprecated), or `{ pressed: boolean }` (deprecated) | Dispatch on the root to programmatically set the pressed state. Blocked when disabled. |

## API Reference

### Options

| Option            | Type                         | Default | Description                            |
| ----------------- | ---------------------------- | ------- | -------------------------------------- |
| `defaultPressed`  | `boolean`                    | `false` | Initial pressed state.                 |
| `disabled`        | `boolean`                    | `false` | Disable user interaction.              |
| `onPressedChange` | `(pressed: boolean) => void` | —       | Called when the pressed state changes. |

### Controller

| Member      | Type               | Description                                                 |
| ----------- | ------------------ | ----------------------------------------------------------- |
| `pressed`   | `boolean` (getter) | Current pressed state.                                      |
| `toggle()`  | `() => void`       | Toggle the pressed state. Always works, even when disabled. |
| `press()`   | `() => void`       | Set pressed to `true`. Always works, even when disabled.    |
| `release()` | `() => void`       | Set pressed to `false`. Always works, even when disabled.   |
| `destroy()` | `() => void`       | Remove all listeners and clean up.                          |

## Controller

The controller is returned by `Toggle.createToggle()` and provides imperative control over the pressed state.

```ts
const controller = Toggle.createToggle(root);

// Toggle
controller.toggle();

// Set pressed to true
controller.press();

// Set pressed to false
controller.release();

// Read current state
console.log(controller.pressed); // true | false

// Clean up
controller.destroy();
```

### Disabled vs. Programmatic Control

User interaction (clicks) and inbound events (`toggle:set`) are blocked when the toggle is disabled. However, the controller methods (`toggle()`, `press()`, `release()`) always work regardless of disabled state. This allows you to have explicit programmatic control while still blocking user input.

### Button vs. Non-Button Elements

- **Button elements**: The controller sets `type="button"` to prevent form submission (unless a `type` is already specified). Button elements handle keyboard interaction (Enter/Space) natively, so no additional keyboard handlers are added.
- **Non-button elements**: The controller sets `role="button"` and `tabindex="0"`. The element's native `click` event (from mouse or keyboard) toggles the state.
