Skip to content
data-slotv1.0.0
Esc
↑↓navigate↵open⌘Jpreview
On this page

Switch

A two-state control for turning a setting on or off.

@data-slot/switchSource ↗
bun add @data-slot/switch
npm install @data-slot/switch
pnpm add @data-slot/switch

Anatomy

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

Preview
Show codeHide 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>

Disabled state

data-disabled blocks interaction while screen readers still report the on/off state.

Preview
Show codeHide 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>

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-checked
  • data-unchecked
  • data-disabled
  • data-readonly
  • data-required

The root also syncs:

  • role="switch"
  • aria-checked="true|false"
  • aria-disabled="true" when disabled
  • aria-readonly="true" when read-only
  • aria-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>