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

Toggle

A button with a pressed state for toggling an option on or off.

@data-slot/toggleSource ↗
bun add @data-slot/toggle
npm install @data-slot/toggle
pnpm add @data-slot/toggle

Anatomy

A single button, no inner parts. The controller keeps aria-pressed and data-state="on|off" in sync.

<button data-slot="toggle">Label</button>

Examples

A pressed state

Preview
Show codeHide code
<div class="toggle-group">
  <button data-slot="toggle" class="toggle-btn">
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
      <path d="M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/>
      <path d="M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/>
    </svg>
  </button>
  <button data-slot="toggle" class="toggle-btn">
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
      <line x1="19" y1="4" x2="10" y2="4"/>
      <line x1="14" y1="20" x2="5" y2="20"/>
      <line x1="15" y1="4" x2="9" y2="20"/>
    </svg>
  </button>
  <button data-slot="toggle" data-default-pressed class="toggle-btn">
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
      <path d="M6 4v16"/>
      <path d="M18 4v16"/>
      <path d="M6 12h12"/>
    </svg>
  </button>
</div>

<style>
  .toggle-group {
    display: flex;
    gap: 0.25rem;
    margin-bottom: 0;
  }
  .toggle-btn {
    padding: 0.5rem;
    background: transparent;
    border: 1px solid var(--border);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: background 0.15s, border-color 0.15s;
  }
  .toggle-btn:hover {
    background: var(--code-bg);
  }
  .toggle-btn[data-state="on"] {
    background: var(--text);
    border-color: var(--text);
    color: var(--surface);
  }
</style>
<div class="flex gap-1">
  <button
    data-slot="toggle"
    class="p-2 bg-transparent border border-[var(--border)] cursor-pointer
           flex items-center justify-center transition-colors
           hover:bg-code-bg data-[state=on]:bg-[var(--text)]
           data-[state=on]:border-[var(--text)] data-[state=on]:text-[var(--surface)]"
  >
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
      <path d="M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/>
      <path d="M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/>
    </svg>
  </button>
  <button
    data-slot="toggle"
    class="p-2 bg-transparent border border-[var(--border)] cursor-pointer
           flex items-center justify-center transition-colors
           hover:bg-code-bg data-[state=on]:bg-[var(--text)]
           data-[state=on]:border-[var(--text)] data-[state=on]:text-[var(--surface)]"
  >
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
      <line x1="19" y1="4" x2="10" y2="4"/>
      <line x1="14" y1="20" x2="5" y2="20"/>
      <line x1="15" y1="4" x2="9" y2="20"/>
    </svg>
  </button>
  <button
    data-slot="toggle"
    data-default-pressed
    class="p-2 bg-transparent border border-[var(--border)] cursor-pointer
           flex items-center justify-center transition-colors
           hover:bg-code-bg data-[state=on]:bg-[var(--text)]
           data-[state=on]:border-[var(--text)] data-[state=on]:text-[var(--surface)]"
  >
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
      <path d="M6 4v16"/>
      <path d="M18 4v16"/>
      <path d="M6 12h12"/>
    </svg>
  </button>
</div>

Disabled state

data-disabled blocks clicks while the pressed state stays visible.

Preview
Show codeHide code
<div class="toggle-group">
  <button data-slot="toggle" data-disabled class="toggle-btn">
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
      <path d="M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/>
      <path d="M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/>
    </svg>
  </button>
  <button data-slot="toggle" data-disabled class="toggle-btn">
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
      <line x1="19" y1="4" x2="10" y2="4"/>
      <line x1="14" y1="20" x2="5" y2="20"/>
      <line x1="15" y1="4" x2="9" y2="20"/>
    </svg>
  </button>
  <button data-slot="toggle" data-disabled data-default-pressed class="toggle-btn">
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
      <path d="M6 4v16"/>
      <path d="M18 4v16"/>
      <path d="M6 12h12"/>
    </svg>
  </button>
</div>

<style>
  .toggle-group {
    display: flex;
    gap: 0.25rem;
    margin-bottom: 0;
  }
  .toggle-btn {
    padding: 0.5rem;
    background: transparent;
    border: 1px solid var(--border);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: background 0.15s, border-color 0.15s;
  }
  .toggle-btn:hover {
    background: var(--code-bg);
  }
  .toggle-btn[data-state="on"] {
    background: var(--text);
    border-color: var(--text);
    color: var(--surface);
  }
</style>
<div class="flex gap-1">
  <button
    data-slot="toggle" data-disabled
    class="p-2 bg-transparent border border-[var(--border)] cursor-pointer
           flex items-center justify-center transition-colors
           hover:bg-code-bg data-[state=on]:bg-[var(--text)]
           data-[state=on]:border-[var(--text)] data-[state=on]:text-[var(--surface)]"
  >
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
      <path d="M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/>
      <path d="M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/>
    </svg>
  </button>
  <button
    data-slot="toggle" data-disabled
    class="p-2 bg-transparent border border-[var(--border)] cursor-pointer
           flex items-center justify-center transition-colors
           hover:bg-code-bg data-[state=on]:bg-[var(--text)]
           data-[state=on]:border-[var(--text)] data-[state=on]:text-[var(--surface)]"
  >
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
      <line x1="19" y1="4" x2="10" y2="4"/>
      <line x1="14" y1="20" x2="5" y2="20"/>
      <line x1="15" y1="4" x2="9" y2="20"/>
    </svg>
  </button>
  <button
    data-slot="toggle" data-disabled
    data-default-pressed
    class="p-2 bg-transparent border border-[var(--border)] cursor-pointer
           flex items-center justify-center transition-colors
           hover:bg-code-bg data-[state=on]:bg-[var(--text)]
           data-[state=on]:border-[var(--text)] data-[state=on]:text-[var(--surface)]"
  >
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
      <path d="M6 4v16"/>
      <path d="M18 4v16"/>
      <path d="M6 12h12"/>
    </svg>
  </button>
</div>

API reference

Initialization

create(scope?)

Auto-discover and bind all toggle instances in a scope (defaults to document).

import { create } from "@data-slot/toggle";

const controllers = create(); // Returns ToggleController[]

createToggle(root, options?)

Create a controller for a specific element.

import { createToggle } from "@data-slot/toggle";

const toggle = createToggle(element, {
  defaultPressed: false,
  disabled: false,
  onPressedChange: (pressed) => console.log(pressed),
});

Slots

Runtime Slots

  • toggle - The button itself. Toggles its pressed state and exposes aria-pressed and data-state for accessibility and styling; no child slots are needed.

Markup

<button data-slot="toggle">Label</button>

The toggle is a simple single-element component. Always use a native <button> element—keyboard support (Enter/Space) and focus handling are only guaranteed with <button>. Non-button elements (e.g., <div>, <span>) are not recommended and would require manual keyboard handling.

Data Attributes

Options can also be set via data attributes on the root element. JS options take precedence.

Attribute Type Default Description
data-default-pressed boolean false Initial pressed state
data-disabled boolean false Disabled state
<!-- Initially pressed toggle -->
<button data-slot="toggle" data-default-pressed>Bold</button>

Options

Option Type Default Description
defaultPressed boolean false Initial pressed state
disabled boolean false Disabled state
onPressedChange (pressed: boolean) => void undefined Callback when state changes

Controller

Method/Property Description
toggle() Toggle the pressed state (ignores disabled)
press() Set pressed to true (ignores disabled)
release() Set pressed to false (ignores disabled)
pressed Current pressed state (readonly boolean)
destroy() Cleanup all event listeners

Note: Controller methods always work, even when disabled. This allows programmatic control regardless of user interaction state. If you need to check disabled state before calling controller methods, check the element’s attributes yourself.

Events

Outbound Events

Listen for changes via custom events:

element.addEventListener("toggle:change", (e) => {
  console.log("Pressed:", e.detail.pressed);
});

Inbound Events

Control the toggle via events (ignored when disabled):

Event Detail Description
toggle:set { value: boolean } Set pressed state programmatically
// Set to specific state
element.dispatchEvent(
  new CustomEvent("toggle:set", { detail: { value: true } })
);

Deprecated Shapes

The following shapes are deprecated and will be removed in v1.0:

// Deprecated: boolean detail
element.dispatchEvent(
  new CustomEvent("toggle:set", { detail: true })
);

// Deprecated: { pressed } shape
element.dispatchEvent(
  new CustomEvent("toggle:set", { detail: { pressed: true } })
);

Use { value: boolean } instead.

Styling

State Attributes

The component sets these attributes for styling:

  • aria-pressed="true|false" - ARIA state
  • data-state="on|off" - CSS styling hook

Basic Styling

/* Unpressed state */
[data-slot="toggle"] {
  background: #e5e7eb;
  border: none;
  padding: 0.5rem 1rem;
  cursor: pointer;
}

/* Pressed state */
[data-slot="toggle"][data-state="on"] {
  background: #3b82f6;
  color: white;
}

/* Or use aria-pressed */
[data-slot="toggle"][aria-pressed="true"] {
  background: #3b82f6;
  color: white;
}

/* Disabled state */
[data-slot="toggle"][aria-disabled="true"] {
  opacity: 0.5;
  cursor: not-allowed;
}

Tailwind Example

<button
  data-slot="toggle"
  class="px-4 py-2 bg-gray-200 data-[state=on]:bg-blue-500 data-[state=on]:text-white aria-disabled:opacity-50"
>
  Bold
</button>

Keyboard Navigation

The toggle uses a native <button> element, so keyboard support is automatic:

Key Action
Enter Toggle state
Space Toggle state

Accessibility

The component automatically handles:

  • aria-pressed state on the button
  • Native disabled and aria-disabled when disabled (for buttons)
  • type="button" to prevent form submission

Behavior

Disabled Behavior

When disabled (via disabled option, data-disabled attribute, native disabled attribute, or aria-disabled="true"):

Input Blocked?
Click / Enter / Space Yes
toggle:set event Yes
Controller methods (toggle(), press(), release()) No

For <button> elements, the disabled option sets both the native disabled attribute and aria-disabled="true". For other elements, only aria-disabled is set.