Skip to content

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.

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

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

Usage

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.

<!-- 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

SlotRequiredDescription
toggleYesRoot element. Receives aria-pressed and state.

Generated Attributes

AttributeElementDescription
data-stateRoot"on" when pressed, "off" when not pressed.
aria-pressedRoot"true" or "false".
aria-disabledRoot"true" when disabled (on all element types).
disabledRootNative attribute set on <button> elements when disabled.
typeRootSet to "button" on <button> elements to prevent form submission (unless already set).
roleRootSet to "button" when the root is not already a <button>.

Events

Outbound

EventDetailDescription
toggle:change{ pressed: boolean }Fired on the root when the pressed state changes.

Inbound

EventDetailDescription
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

OptionTypeDefaultDescription
defaultPressedbooleanfalseInitial pressed state.
disabledbooleanfalseDisable user interaction.
onPressedChange(pressed: boolean) => voidCalled when the pressed state changes.

Controller

MemberTypeDescription
pressedboolean (getter)Current pressed state.
toggle()() => voidToggle the pressed state. Always works, even when disabled.
press()() => voidSet pressed to true. Always works, even when disabled.
release()() => voidSet pressed to false. Always works, even when disabled.
destroy()() => voidRemove all listeners and clean up.

Controller

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();

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.