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.
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 { Toggle } from "@areia/slots";
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();
The toggle is a single-element primitive — there are no sub-slots. It can be a native <button> or any other focusable element.
<!-- 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.
| Slot | Required | Description |
|---|
toggle | Yes | Root element. Receives aria-pressed and state. |
| 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>. |
| Event | Detail | Description |
|---|
toggle:change | { pressed: boolean } | Fired on the root when the pressed state changes. |
| 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. |
| 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. |
| 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. |
The controller is returned by Toggle.createToggle() and provides imperative control over the pressed state.
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();
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 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.