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
| Slot | Required | Description |
|---|---|---|
switch | Yes | Root element. Receives role="switch" and ARIA attrs. |
switch-thumb | No | Visual thumb indicator. Receives matching state attrs. |
Generated Attributes
| Attribute | Element(s) | Description |
|---|---|---|
data-checked | Root, thumb | Present when checked. |
data-unchecked | Root, thumb | Present when unchecked. |
data-disabled | Root, thumb | Present when disabled. |
data-readonly | Root, thumb | Present when read-only. |
data-required | Root, thumb | Present when required. |
role | Root | Set to "switch". |
aria-checked | Root | "true" or "false". |
aria-disabled | Root | "true" or removed. |
aria-readonly | Root | "true" or removed. |
aria-required | Root | "true" or removed. |
aria-labelledby | Root | Merged from wrapping <label> and [for] associations. |
Events
Outbound
| Event | Detail | Description |
|---|---|---|
switch:change | { checked: boolean } | Fired on the root when the checked state changes via user interaction or the controller. |
Inbound
| Event | Detail | Description |
|---|---|---|
switch:set | boolean or { checked: boolean } | Dispatch on the root to set the checked state. Supports flat boolean shorthand. |
API Reference
Options
| Option | Type | Default | Description |
|---|---|---|---|
defaultChecked | boolean | false | Initial checked state. |
disabled | boolean | false | Disable user interaction and exclude from form submission. |
readOnly | boolean | false | Prevent user interaction while keeping the value submittable. |
required | boolean | false | Require a checked value for native form validation. |
name | string | — | Form field name. |
value | string | — | Submitted value when checked. Defaults to the native checkbox "on". |
uncheckedValue | string | — | Submitted value when unchecked. Creates an additional hidden input. |
onCheckedChange | (checked: boolean) => void | — | Called when the checked state changes. |
Controller
| Member | Type | Description |
|---|---|---|
checked | boolean (getter) | Current checked state. |
toggle() | () => void | Toggle the checked state. |
check() | () => void | Set checked to true. |
uncheck() | () => void | Set checked to false. |
setChecked(checked) | (checked: boolean) => void | Set the checked state. |
destroy() | () => void | Remove 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 receivestype="button"to prevent unintended form submission and gets thedisabledattribute 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.