Skip to content

Switch

A native-feeling switch built on a role="switch" element. It generates a hidden native <input type="checkbox"> for form submission, supports unchecked-value submission, and emits change events on user interaction.

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

document.querySelector("#root")!.innerHTML = `
  <label>
    <span data-slot="switch" class="mr-2 inline-flex h-6 w-11 items-center border-2 border-t-black border-l-black border-r-white border-b-white bg-white p-0.5 align-middle data-[checked]:justify-end">
      <span data-slot="switch-thumb" class="block size-4 border border-black bg-[#c0c0c0]"></span>
    </span>
    Enable notifications
  </label>
`;

const root = document.querySelector('[data-slot="switch"]')!;
const controller = Switch.createSwitch(root, {
  defaultChecked: false,
  name: "notifications",
});

Import

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

Usage

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

// Auto-bind a single root
const root = document.querySelector('[data-slot="switch"]');
const controller = Switch.createSwitch(root, {
  name: "accept",
  value: "yes",
});

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

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

Expected Markup

The root element receives role="switch" and keyboard handling. A hidden native <input type="checkbox"> is generated and inserted after the root for form participation. The optional switch-thumb slot receives matching state attributes.

<label>
  <span data-slot="switch">
    <span data-slot="switch-thumb"></span>
  </span>
  Enable notifications
</label>

Wrapping the switch in a <label> is recommended — the controller detects wrapping labels and label[for] associations so that clicking the label text toggles the switch.

Minimal Markup (no thumb)

<span data-slot="switch"></span>

Generated Hidden Input

The controller creates a visually hidden native checkbox and inserts it immediately after the root element:

<input
  type="checkbox"
  tabindex="-1"
  aria-hidden="true"
  data-switch-generated="input"
  style="position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;pointer-events:none"
/>

When uncheckedValue is configured, a second hidden input is generated to submit a value even when the switch is unchecked.

Data Slots

SlotRequiredDescription
switchYesRoot element. Receives role="switch" and ARIA attrs.
switch-thumbNoVisual thumb indicator. Receives matching state attrs.

Generated Attributes

AttributeElement(s)Description
data-checkedRoot, thumbPresent when checked.
data-uncheckedRoot, thumbPresent when unchecked.
data-disabledRoot, thumbPresent when disabled.
data-readonlyRoot, thumbPresent when read-only.
data-requiredRoot, thumbPresent when required.
roleRootSet to "switch".
aria-checkedRoot"true" or "false".
aria-disabledRoot"true" or removed.
aria-readonlyRoot"true" or removed.
aria-requiredRoot"true" or removed.
aria-labelledbyRootMerged from wrapping <label> and [for] associations.

Events

Outbound

EventDetailDescription
switch:change{ checked: boolean }Fired on the root when the checked state changes via user interaction or the controller.

Inbound

EventDetailDescription
switch:setboolean or { checked: boolean }Dispatch on the root to set the checked state. Supports flat boolean shorthand.

API Reference

Options

OptionTypeDefaultDescription
defaultCheckedbooleanfalseInitial checked state.
disabledbooleanfalseDisable user interaction and exclude from form submission.
readOnlybooleanfalsePrevent user interaction while keeping the value submittable.
requiredbooleanfalseRequire a checked value for native form validation.
namestringForm field name.
valuestringSubmitted value when checked. Defaults to the native checkbox "on".
uncheckedValuestringSubmitted value when unchecked. Creates an additional hidden input.
onCheckedChange(checked: boolean) => voidCalled when the checked state changes.

Controller

MemberTypeDescription
checkedboolean (getter)Current checked state.
toggle()() => voidToggle the checked state.
check()() => voidSet checked to true.
uncheck()() => voidSet checked to false.
setChecked(checked)(checked: boolean) => voidSet the checked state.
destroy()() => voidRemove listeners and generated inputs.

Controller

The controller is returned by Switch.createSwitch() and provides imperative control over the switch state.

const controller = Switch.createSwitch(root, { name: "terms" });

// Toggle
controller.toggle();

// Force check
controller.check();

// Force uncheck
controller.uncheck();

// Set checked state
controller.setChecked(true);

// Read state
console.log(controller.checked); // true

// Clean up
controller.destroy();

Form Integration

The generated hidden input participates in standard form submission and FormData. When uncheckedValue is set, an extra hidden input ensures a value is submitted even when the switch is unchecked. The controller also listens for the form's reset event and restores the initial default state.

Keyboard Interaction

  • Space and Enter toggle the switch.
  • The root element receives tabindex="0" when it is not already a naturally focusable element (such as a <button>).
  • If the root is a native <button>, it receives type="button" to prevent unintended form submission and gets the disabled attribute set directly.

Label Support

The controller detects both wrapping <label> elements and explicit label[for] associations. Clicking an associated label toggles the switch, and the label's ID is merged into the root's aria-labelledby.