Switch
A two-state control for turning a setting on or off.
@data-slot/switchSource ↗bun add @data-slot/switchnpm install @data-slot/switchpnpm add @data-slot/switchAnatomy
Just a root and an optional thumb. The controller injects a visually hidden checkbox next to the root, so the switch works inside a <label> and submits with a form like a native one.
<span data-slot="switch">
<span data-slot="switch-thumb"></span>
</span>
Examples
Toggle a setting
Show code
<div class="switch-stack">
<label class="switch-row">
<span data-slot="switch" data-default-checked class="switch-root">
<span data-slot="switch-thumb" class="switch-thumb"></span>
</span>
Release updates
</label>
<label class="switch-row">
<span data-slot="switch" class="switch-root switch-root--compact">
<span data-slot="switch-thumb" class="switch-thumb switch-thumb--compact"></span>
</span>
Compact alerts
</label>
</div>
<style>
.switch-stack {
display: grid;
gap: 0.75rem;
}
.switch-row {
display: inline-flex;
align-items: center;
gap: 0.75rem;
color: var(--text);
font-size: 0.875rem;
line-height: 1.25rem;
}
.switch-root {
position: relative;
display: inline-flex;
width: 32px;
height: 18px;
align-items: center;
border-radius: 999px;
background: var(--muted);
padding: 1px;
transition: background-color 150ms ease;
outline: none;
flex-shrink: 0;
}
.switch-root[data-checked] {
background: var(--text);
}
.switch-root--compact {
width: 24px;
height: 14px;
}
.switch-thumb {
display: block;
width: 16px;
height: 16px;
border-radius: 999px;
background: var(--surface);
box-shadow: 0 1px 3px rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
transition: transform 150ms ease;
}
.switch-thumb[data-checked] {
transform: translateX(14px);
}
.switch-thumb--compact {
width: 12px;
height: 12px;
}
.switch-root--compact .switch-thumb[data-checked] {
transform: translateX(10px);
}
.switch-root:focus-visible { box-shadow: 0 0 0 2px color-mix(in srgb, var(--muted) 70%, transparent); }
</style><div class="grid gap-3">
<label class="inline-flex items-center gap-3 text-sm text-text">
<span
data-slot="switch"
data-default-checked
data-size="default"
class="data-checked:bg-text data-unchecked:bg-[var(--muted)] focus-visible:ring-[var(--muted)]/70 group/switch relative inline-flex h-4.5 w-8 shrink-0 items-center rounded-full p-px outline-none transition-colors focus-visible:ring-2"
>
<span
data-slot="switch-thumb"
class="pointer-events-none block size-4 rounded-full bg-[var(--surface)] shadow-sm transition-transform data-checked:translate-x-3.5 data-unchecked:translate-x-0"
></span>
</span>
Release updates
</label>
<label class="inline-flex items-center gap-3 text-sm text-text">
<span
data-slot="switch"
data-size="sm"
class="data-checked:bg-text data-unchecked:bg-[var(--muted)] focus-visible:ring-[var(--muted)]/70 group/switch relative inline-flex h-3.5 w-6 shrink-0 items-center rounded-full p-px outline-none transition-colors focus-visible:ring-2"
>
<span
data-slot="switch-thumb"
class="pointer-events-none block size-3 rounded-full bg-[var(--surface)] shadow-sm transition-transform data-checked:translate-x-2.5 data-unchecked:translate-x-0"
></span>
</span>
Compact alerts
</label>
</div>import { create } from "@data-slot/switch";
// Initialize after the markup is in the document.
const controllers = create();
// Clean up before removing the component.
// controllers.forEach((controller) => controller.destroy());Disabled state
data-disabled blocks interaction while screen readers still report the on/off state.
Show code
<div class="switch-stack">
<label class="switch-row">
<span data-slot="switch" data-disabled data-default-checked class="switch-root">
<span data-slot="switch-thumb" class="switch-thumb"></span>
</span>
Release updates
</label>
<label class="switch-row">
<span data-slot="switch" data-disabled class="switch-root switch-root--compact">
<span data-slot="switch-thumb" class="switch-thumb switch-thumb--compact"></span>
</span>
Compact alerts
</label>
</div>
<style>
.switch-stack {
display: grid;
gap: 0.75rem;
}
.switch-row {
display: inline-flex;
align-items: center;
gap: 0.75rem;
color: var(--text);
font-size: 0.875rem;
line-height: 1.25rem;
}
.switch-root {
position: relative;
display: inline-flex;
width: 32px;
height: 18px;
align-items: center;
border-radius: 999px;
background: var(--muted);
padding: 1px;
transition: background-color 150ms ease;
outline: none;
flex-shrink: 0;
}
.switch-root[data-checked] {
background: var(--text);
}
.switch-root--compact {
width: 24px;
height: 14px;
}
.switch-thumb {
display: block;
width: 16px;
height: 16px;
border-radius: 999px;
background: var(--surface);
box-shadow: 0 1px 3px rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
transition: transform 150ms ease;
}
.switch-thumb[data-checked] {
transform: translateX(14px);
}
.switch-thumb--compact {
width: 12px;
height: 12px;
}
.switch-root--compact .switch-thumb[data-checked] {
transform: translateX(10px);
}
.switch-root:focus-visible { box-shadow: 0 0 0 2px color-mix(in srgb, var(--muted) 70%, transparent); }
</style><div class="grid gap-3">
<label class="inline-flex items-center gap-3 text-sm text-text">
<span
data-slot="switch" data-disabled
data-default-checked
data-size="default"
class="data-checked:bg-text data-unchecked:bg-[var(--muted)] focus-visible:ring-[var(--muted)]/70 group/switch relative inline-flex h-4.5 w-8 shrink-0 items-center rounded-full p-px outline-none transition-colors focus-visible:ring-2"
>
<span
data-slot="switch-thumb"
class="pointer-events-none block size-4 rounded-full bg-[var(--surface)] shadow-sm transition-transform data-checked:translate-x-3.5 data-unchecked:translate-x-0"
></span>
</span>
Release updates
</label>
<label class="inline-flex items-center gap-3 text-sm text-text">
<span
data-slot="switch" data-disabled
data-size="sm"
class="data-checked:bg-text data-unchecked:bg-[var(--muted)] focus-visible:ring-[var(--muted)]/70 group/switch relative inline-flex h-3.5 w-6 shrink-0 items-center rounded-full p-px outline-none transition-colors focus-visible:ring-2"
>
<span
data-slot="switch-thumb"
class="pointer-events-none block size-3 rounded-full bg-[var(--surface)] shadow-sm transition-transform data-checked:translate-x-2.5 data-unchecked:translate-x-0"
></span>
</span>
Compact alerts
</label>
</div>import { create } from "@data-slot/switch";
// Initialize after the markup is in the document.
const controllers = create();
// Clean up before removing the component.
// controllers.forEach((controller) => controller.destroy());API reference
Initialization
create(scope?)
Auto-discover and bind all switch instances in a scope (defaults to document).
import { create } from "@data-slot/switch";
const controllers = create(); // Returns SwitchController[]
createSwitch(root, options?)
Create a controller for a specific element.
import { createSwitch } from "@data-slot/switch";
const controller = createSwitch(element, {
defaultChecked: true,
name: "notifications",
uncheckedValue: "off",
onCheckedChange: (checked) => console.log(checked),
});
Slots
Runtime Slots
switch- Root control that toggles the checked state and receives switch semantics, keyboard handling, and form integration.switch-thumb- Optional visual thumb inside the root; receives checked and disabled state for styling.
Markup
<span data-slot="switch">
<span data-slot="switch-thumb"></span>
</span>
Use a neutral root element (span or div) when you want Base UI-style label wrapping and shadcn-like composition. The controller injects a visually hidden checkbox next to the root for form submission, label support, and native validation.
Data Attributes
JS options take precedence over data attributes on the root element.
| Attribute | Type | Default | Description |
|---|---|---|---|
data-default-checked |
boolean |
false |
Initial checked state |
data-disabled |
boolean |
false |
Disable user interaction and form submission |
data-read-only / data-readOnly |
boolean |
false |
Prevent user interaction while keeping the field enabled |
data-required |
boolean |
false |
Require a checked value |
data-name |
string |
- | Form field name |
data-value |
string |
native checkbox "on" |
Submitted value when checked |
data-unchecked-value / data-uncheckedValue |
string |
- | Submitted value when unchecked |
Options
| Option | Type | Default | Description |
|---|---|---|---|
defaultChecked |
boolean |
false |
Initial checked state |
disabled |
boolean |
false |
Disable user interaction and form submission |
readOnly |
boolean |
false |
Prevent user interaction while keeping the field enabled |
required |
boolean |
false |
Require the switch to be checked for native form validation |
name |
string |
- | Form field name |
value |
string |
native checkbox "on" |
Submitted value when checked |
uncheckedValue |
string |
- | Submitted value when unchecked |
onCheckedChange |
(checked: boolean) => void |
undefined |
Callback when checked state changes |
Controller
| Method/Property | Description |
|---|---|
checked |
Current checked state (readonly boolean) |
toggle() |
Toggle the checked state |
check() |
Set checked to true |
uncheck() |
Set checked to false |
setChecked(checked) |
Set the checked state explicitly |
destroy() |
Remove listeners and generated inputs |
Events
Outbound Events
element.addEventListener("switch:change", (event) => {
console.log(event.detail.checked);
});
Inbound Events
element.dispatchEvent(
new CustomEvent("switch:set", { detail: { checked: true } })
);
Styling
State Attributes
The root and thumb expose presence attributes:
data-checkeddata-uncheckeddata-disableddata-readonlydata-required
The root also syncs:
role="switch"aria-checked="true|false"aria-disabled="true"when disabledaria-readonly="true"when read-onlyaria-required="true"when required
Tailwind Example
<label class="inline-flex items-center gap-3">
<span
data-slot="switch"
data-size="default"
class="data-checked:bg-primary data-unchecked:bg-input
focus-visible:border-ring focus-visible:ring-ring/50
shrink-0 rounded-full p-px
focus-visible:ring-3 peer group/switch relative
inline-flex items-center transition-all outline-none
h-4.5 w-8"
>
<span
data-slot="switch-thumb"
class="bg-background rounded-full size-4
data-checked:translate-x-3.5
data-unchecked:translate-x-0
pointer-events-none block transition-transform"
></span>
</span>
Notifications
</label>