settings
examples mode:
/
ver: 0.2.90
github · npm
data-slot

headless UI components for vanilla JavaScript.

tiny · accessible · unstyled
~~~

PACKAGES

+----------------------------+--------+-----------------------------+
| Package                    |   Size | Description                 |
|----------------------------+--------+-----------------------------|
| @data-slot/navigation-menu | 6.2 KB | Dropdown navigation menus   |
| @data-slot/core            | 4.5 KB | Shared utilities            |
| @data-slot/combobox        | 3.7 KB | Autocomplete input          |
| @data-slot/select          | 3.7 KB | Dropdown select, form-ready |
| @data-slot/dropdown-menu   | 2.4 KB | Action menus, kbd nav       |
| @data-slot/slider          | 2.3 KB | Single/range value sliders  |
| @data-slot/tabs            | 1.8 KB | Tabbed interfaces, kbd nav  |
| @data-slot/tooltip         | 1.8 KB | Hover/focus tooltips        |
| @data-slot/popover         | 1.8 KB | Anchored floating content   |
| @data-slot/dialog          | 1.8 KB | Modal dialogs, focus trap   |
| @data-slot/collapsible     | 1.5 KB | Simple show/hide toggle     |
| @data-slot/accordion       | 1.3 KB | Collapsible sections        |
+----------------------------+--------+-----------------------------+

INSTALLATION

Install only what you need:

bun add @data-slot/tabs
bun add @data-slot/dialog

QUICK START

Components use data-slot attributes for markup:

<div data-slot="tabs" data-default-value="one">
  <div data-slot="tabs-list">
    <button data-slot="tabs-trigger" data-value="one">Tab One</button>
    <button data-slot="tabs-trigger" data-value="two">Tab Two</button>
  </div>
  <div data-slot="tabs-content" data-value="one">Content One</div>
  <div data-slot="tabs-content" data-value="two">Content Two</div>
</div>

Import and call create() to bind behavior:

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

// Auto-discover and bind all [data-slot="tabs"] elements
const controllers = create();

// Or target a specific element
import { createTabs } from "@data-slot/tabs";
const tabs = createTabs(element);

tabs.select("two");   // programmatic control
tabs.destroy();       // cleanup
~~~

LIVE DEMOS

Interactive examples using the library. Inspect the source for markup patterns.

Tabs

↓ interactive
<div data-slot="tabs">
  <div data-slot="tabs-list">
    <button data-slot="tabs-trigger" data-value="one">Tab</button>
  </div>
  <div data-slot="tabs-content" data-value="one">...</div>
</div>
/* Use data-state for styling */
[data-slot="tabs-trigger"][data-state="active"] {
  font-weight: bold;
  border-bottom: 2px solid;
}
import { create } from "@data-slot/tabs";

const [tabs] = create();
tabs.select("css");
console.log(tabs.value); // "css"
View source
<div data-slot="tabs" data-default-value="one">
  <div data-slot="tabs-list" class="tabs-list">
    <button data-slot="tabs-trigger" data-value="one" class="tabs-trigger">Tab One</button>
    <button data-slot="tabs-trigger" data-value="two" class="tabs-trigger">Tab Two</button>
  </div>
  <div data-slot="tabs-content" data-value="one" class="tabs-content">Content One</div>
  <div data-slot="tabs-content" data-value="two" class="tabs-content">Content Two</div>
</div>

<style>
  .tabs-list {
    display: flex;
    border-bottom: 1px solid #ccc;
    margin-bottom: 1rem;
  }
  .tabs-trigger {
    padding: 0.5rem 1rem;
    background: none;
    border: none;
    border-bottom: 2px solid transparent;
    cursor: pointer;
    color: #666;
  }
  .tabs-trigger[data-state="active"] {
    color: #1a1a1a;
    border-bottom-color: #1a1a1a;
    font-weight: 500;
  }
  .tabs-content { padding: 1rem 0; }
  .tabs-content[hidden] { display: none; }
</style>
<div data-slot="tabs" data-default-value="one">
  <div data-slot="tabs-list" class="flex border-b border-gray-300 mb-4">
    <button
      data-slot="tabs-trigger"
      data-value="one"
      class="px-4 py-2 border-b-2 border-transparent text-gray-500 cursor-pointer
             data-[state=active]:text-gray-900 data-[state=active]:border-gray-900
             data-[state=active]:font-medium"
    >
      Tab One
    </button>
    <button
      data-slot="tabs-trigger"
      data-value="two"
      class="px-4 py-2 border-b-2 border-transparent text-gray-500 cursor-pointer
             data-[state=active]:text-gray-900 data-[state=active]:border-gray-900
             data-[state=active]:font-medium"
    >
      Tab Two
    </button>
  </div>
  <div data-slot="tabs-content" data-value="one" class="py-4 hidden data-[state=active]:block">
    Content One
  </div>
  <div data-slot="tabs-content" data-value="two" class="py-4 hidden data-[state=active]:block">
    Content Two
  </div>
</div>
↓ with animated indicator
The indicator slides smoothly to the active tab using CSS transitions. Position is set via CSS variables: --active-tab-left and --active-tab-width.
Zero JavaScript for animation — just CSS transition on transform and width. Works with keyboard navigation too.
Add data-slot="tabs-indicator" inside your tabs-list. The component sets CSS variables automatically.
View source
<div data-slot="tabs" data-default-value="overview">
  <div data-slot="tabs-list" class="tabs-list-indicator">
    <div data-slot="tabs-indicator" class="tabs-indicator"></div>
    <button data-slot="tabs-trigger" data-value="overview" class="tabs-trigger">Overview</button>
    <button data-slot="tabs-trigger" data-value="features" class="tabs-trigger">Features</button>
    <button data-slot="tabs-trigger" data-value="api" class="tabs-trigger">API</button>
  </div>
  <div data-slot="tabs-content" data-value="overview">Content</div>
</div>

<style>
  .tabs-list-indicator {
    position: relative;
    display: flex;
    gap: 0.25rem;
    padding: 0.25rem;
    background: #f0eeeb;
    border-radius: 6px;
    overflow: auto;
  }
  .tabs-indicator {
    position: absolute;
    top: 0.25rem;
    height: calc(100% - 0.5rem);
    background: #fff;
    border-radius: 4px;
    box-shadow: 0 1px 3px rgba(0,0,0,0.12);
    transform: translateX(var(--active-tab-left, 0));
    width: var(--active-tab-width, 0);
    transition: transform 0.2s, width 0.2s;
  }
  .tabs-trigger {
    position: relative;
    z-index: 1;
    padding: 0.375rem 0.75rem;
    background: none;
    border: none;
    cursor: pointer;
  }
</style>
<div data-slot="tabs" data-default-value="overview">
  <div
    data-slot="tabs-list"
    class="relative flex gap-1 p-1 bg-code-bg rounded-md mb-4"
  >
    <div
      data-slot="tabs-indicator"
      class="absolute top-1 h-[calc(100%-0.5rem)] bg-white rounded shadow
             transition-all duration-200
             [transform:translateX(var(--active-tab-left,0))]
             [width:var(--active-tab-width,0)]"
    ></div>
    <button
      data-slot="tabs-trigger"
      data-value="overview"
      class="relative z-10 px-3 py-1.5 bg-transparent border-none cursor-pointer"
    >
      Overview
    </button>
    <button
      data-slot="tabs-trigger"
      data-value="features"
      class="relative z-10 px-3 py-1.5 bg-transparent border-none cursor-pointer"
    >
      Features
    </button>
    <button
      data-slot="tabs-trigger"
      data-value="api"
      class="relative z-10 px-3 py-1.5 bg-transparent border-none cursor-pointer"
    >
      API
    </button>
  </div>
  <div data-slot="tabs-content" data-value="overview">Content</div>
</div>

Accordion

↓ single mode (default)
Unlike React-based headless libraries, data-slot works with vanilla HTML. Just add data-slot attributes to your markup and call create(). No framework, no virtual DOM, no build step required.
Yes. All components implement WAI-ARIA patterns with proper roles, keyboard navigation, and focus management out of the box.
No dependencies. Each component is standalone and tree-shakeable.
Unlike React-based headless libraries, data-slot works with vanilla HTML. Just add data-slot attributes to your markup and call create(). No framework, no virtual DOM, no build step required.
Yes. All components implement WAI-ARIA patterns with proper roles, keyboard navigation, and focus management out of the box.
No dependencies. Each component is standalone and tree-shakeable.
View source
<div data-slot="accordion" data-default-value="features">
  <div data-slot="accordion-item" data-value="features" class="accordion-item">
    <button data-slot="accordion-trigger" class="accordion-trigger">
      What makes data-slot different?
    </button>
    <div class="accordion-content-wrapper">
      <div data-slot="accordion-content" class="accordion-content">
        <div class="accordion-content-inner">
          Unlike React-based headless libraries, data-slot works with vanilla HTML.
        </div>
      </div>
    </div>
  </div>
  <div data-slot="accordion-item" data-value="accessibility" class="accordion-item">
    <button data-slot="accordion-trigger" class="accordion-trigger">Is it accessible?</button>
    <div class="accordion-content-wrapper">
      <div data-slot="accordion-content" class="accordion-content">
        <div class="accordion-content-inner">
          Yes. All components implement WAI-ARIA patterns.
        </div>
      </div>
    </div>
  </div>
</div>

<style>
  .accordion-item { border-bottom: 1px dashed #ccc; }
  .accordion-item:last-child { border-bottom: none; }
  .accordion-trigger {
    width: 100%;
    padding: 0.75rem 0;
    background: none;
    border: none;
    text-align: left;
    cursor: pointer;
    display: flex;
    justify-content: space-between;
  }
  .accordion-trigger::after {
    content: "+";
    color: #666;
    transition: transform 0.25s;
  }
  .accordion-trigger[aria-expanded="true"]::after {
    transform: rotate(45deg);
  }
  .accordion-content-wrapper {
    display: grid;
    grid-template-rows: 0fr;
    transition: grid-template-rows 0.3s;
  }
  .accordion-item[data-state="open"] .accordion-content-wrapper {
    grid-template-rows: 1fr;
  }
  .accordion-content {
    overflow: hidden;
    min-height: 0;
  }
  .accordion-content[hidden] { display: block !important; }
  .accordion-content-inner { padding: 0 0 1rem; color: #666; }
</style>
<div data-slot="accordion" data-default-value="features">
  <div
    data-slot="accordion-item"
    data-value="features"
    class="border-b border-dashed border-gray-300 last:border-b-0
           group"
  >
    <button
      data-slot="accordion-trigger"
      class="w-full py-3 bg-transparent border-none text-left cursor-pointer
             flex justify-between items-center
             after:content-['+'] after:text-gray-500
             after:transition-transform after:duration-300
             aria-expanded:after:rotate-45"
    >
      What makes data-slot different?
    </button>
    <div
      class="grid grid-rows-[0fr] transition-[grid-template-rows] duration-300
             group-data-[state=open]:grid-rows-[1fr]"
    >
      <div data-slot="accordion-content" class="overflow-hidden min-h-0">
        <div class="pb-4 text-gray-500">
          Unlike React-based headless libraries, data-slot works with vanilla HTML.
        </div>
      </div>
    </div>
  </div>
  <div
    data-slot="accordion-item"
    data-value="accessibility"
    class="border-b border-dashed border-gray-300 last:border-b-0
           group"
  >
    <button
      data-slot="accordion-trigger"
      class="w-full py-3 bg-transparent border-none text-left cursor-pointer
             flex justify-between items-center
             after:content-['+'] after:text-gray-500
             after:transition-transform after:duration-300
             aria-expanded:after:rotate-45"
    >
      Is it accessible?
    </button>
    <div
      class="grid grid-rows-[0fr] transition-[grid-template-rows] duration-300
             group-data-[state=open]:grid-rows-[1fr]"
    >
      <div data-slot="accordion-content" class="overflow-hidden min-h-0">
        <div class="pb-4 text-gray-500">
          Yes. All components implement WAI-ARIA patterns.
        </div>
      </div>
    </div>
  </div>
</div>
↓ multiple mode
With multiple: true, multiple items can be open simultaneously.
Try clicking both items — they stay open independently.
With multiple: true, multiple items can be open simultaneously.
Try clicking both items — they stay open independently.
View source
<div data-slot="accordion">
  <div data-slot="accordion-item" data-value="item1" class="accordion-item">
    <button data-slot="accordion-trigger" class="accordion-trigger">First item</button>
    <div class="accordion-content-wrapper">
      <div data-slot="accordion-content" class="accordion-content">
        <div class="accordion-content-inner">
          With <code>multiple: true</code>, multiple items can be open simultaneously.
        </div>
      </div>
    </div>
  </div>
  <div data-slot="accordion-item" data-value="item2" class="accordion-item">
    <button data-slot="accordion-trigger" class="accordion-trigger">Second item</button>
    <div class="accordion-content-wrapper">
      <div data-slot="accordion-content" class="accordion-content">
        <div class="accordion-content-inner">
          Try clicking both items — they stay open independently.
        </div>
      </div>
    </div>
  </div>
</div>

<script type="module">
  import { createAccordion } from '@data-slot/accordion';
  createAccordion(element, { multiple: true });
</script>
<div data-slot="accordion">
  <div
    data-slot="accordion-item"
    data-value="item1"
    class="border-b border-dashed border-gray-300 last:border-b-0 group"
  >
    <button
      data-slot="accordion-trigger"
      class="w-full py-3 bg-transparent border-none text-left cursor-pointer
             flex justify-between items-center
             after:content-['+'] after:text-gray-500
             after:transition-transform after:duration-300
             aria-expanded:after:rotate-45"
    >
      First item
    </button>
    <div
      class="grid grid-rows-[0fr] transition-[grid-template-rows] duration-300
             group-data-[state=open]:grid-rows-[1fr]"
    >
      <div data-slot="accordion-content" class="overflow-hidden min-h-0">
        <div class="pb-4 text-gray-500">
          With <code>multiple: true</code>, multiple items can be open simultaneously.
        </div>
      </div>
    </div>
  </div>
  <div
    data-slot="accordion-item"
    data-value="item2"
    class="border-b border-dashed border-gray-300 last:border-b-0 group"
  >
    <button
      data-slot="accordion-trigger"
      class="w-full py-3 bg-transparent border-none text-left cursor-pointer
             flex justify-between items-center
             after:content-['+'] after:text-gray-500
             after:transition-transform after:duration-300
             aria-expanded:after:rotate-45"
    >
      Second item
    </button>
    <div
      class="grid grid-rows-[0fr] transition-[grid-template-rows] duration-300
             group-data-[state=open]:grid-rows-[1fr]"
    >
      <div data-slot="accordion-content" class="overflow-hidden min-h-0">
        <div class="pb-4 text-gray-500">
          Try clicking both items — they stay open independently.
        </div>
      </div>
    </div>
  </div>
</div>

<script type="module">
  import { createAccordion } from '@data-slot/accordion';
  createAccordion(element, { multiple: true });
</script>

Dialog

↓ interactive
View source
<div data-slot="dialog">
  <button data-slot="dialog-trigger" class="dialog-trigger-btn">Open Dialog</button>
  <div data-slot="dialog-overlay" class="dialog-overlay" hidden></div>
  <div data-slot="dialog-content" class="dialog-panel" hidden>
    <h2 data-slot="dialog-title" class="dialog-title">Confirm Action</h2>
    <p data-slot="dialog-description" class="dialog-description">
      This dialog traps focus and can be closed with Escape or clicking outside.
    </p>
    <button data-slot="dialog-close" class="dialog-close-btn">Close</button>
  </div>
</div>

<style>
  .dialog-trigger-btn {
    padding: 0.5rem 1rem;
    background: #1a1a1a;
    color: #faf9f7;
    border: none;
    cursor: pointer;
  }
  .dialog-overlay {
    position: fixed;
    inset: 0;
    background: rgba(0,0,0,0.5);
    z-index: 100;
  }
  .dialog-overlay[hidden] { display: none; }
  .dialog-panel {
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    background: #faf9f7;
    padding: 2rem;
    max-width: 400px;
    width: calc(100% - 2rem);
    border: 1px solid #ccc;
    z-index: 101;
  }
  .dialog-panel[hidden] { display: none; }
  .dialog-title { font-weight: 700; margin-bottom: 0.5rem; }
  .dialog-description { color: #666; margin-bottom: 1.5rem; }
  .dialog-close-btn {
    padding: 0.4rem 0.8rem;
    background: none;
    border: 1px solid #ccc;
    cursor: pointer;
  }
</style>
<div data-slot="dialog" class="group/dialog">
  <button
    data-slot="dialog-trigger"
    class="px-4 py-2 bg-gray-900 text-white border-none cursor-pointer
           hover:opacity-90 transition-opacity"
  >
    Open Dialog
  </button>
  <div
    data-slot="dialog-overlay"
    class="fixed inset-0 bg-black/50 z-50 opacity-0
           group-data-[state=open]/dialog:opacity-100
           transition-opacity duration-200"
    hidden
  ></div>
  <div
    data-slot="dialog-content"
    class="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2
           bg-white p-8 max-w-md w-[calc(100%-2rem)] border border-gray-300
           rounded-lg z-50 opacity-0 scale-95
           group-data-[state=open]/dialog:opacity-100
           group-data-[state=open]/dialog:scale-100
           transition-all duration-200"
    hidden
  >
    <h2 data-slot="dialog-title" class="font-bold mb-2 mt-0">Confirm Action</h2>
    <p data-slot="dialog-description" class="text-gray-500 mb-6">
      This dialog traps focus and can be closed with Escape or clicking outside.
    </p>
    <button
      data-slot="dialog-close"
      class="px-3 py-1.5 bg-transparent border border-gray-300 cursor-pointer
             hover:bg-code-bg transition-colors"
    >
      Close
    </button>
  </div>
</div>

/* CSS override needed to allow transitions with hidden attribute */
[data-slot="dialog-overlay"][hidden],
[data-slot="dialog-content"][hidden] {
  display: block !important;
  pointer-events: none;
}

Collapsible

↓ interactive

Collapsible is the simplest component — just a trigger and content.

Perfect for FAQ items, collapsible sections, or any show/hide pattern.

Collapsible is the simplest component — just a trigger and content.

Perfect for FAQ items, collapsible sections, or any show/hide pattern.

View source
<div data-slot="collapsible">
  <button data-slot="collapsible-trigger" class="collapsible-trigger-btn">
    Show more details
  </button>
  <div class="collapsible-content-wrapper">
    <div data-slot="collapsible-content" class="collapsible-content">
      <div class="collapsible-content-inner">
        <p>Collapsible is the simplest component — just a trigger and content.</p>
        <p>Perfect for FAQ items, collapsible sections, or any show/hide pattern.</p>
      </div>
    </div>
  </div>
</div>

<style>
  .collapsible-trigger-btn {
    padding: 0.5rem 1rem;
    background: none;
    border: 1px dashed #ccc;
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: 0.5rem;
  }
  .collapsible-trigger-btn::before {
    content: "";
    font-size: 0.7rem;
    transition: transform 0.2s;
  }
  .collapsible-trigger-btn[aria-expanded="true"]::before {
    transform: rotate(90deg);
  }
  .collapsible-content-wrapper {
    display: grid;
    grid-template-rows: 0fr;
    transition: grid-template-rows 0.3s;
  }
  [data-slot="collapsible"][data-state="open"] .collapsible-content-wrapper {
    grid-template-rows: 1fr;
  }
  .collapsible-content {
    overflow: hidden;
    min-height: 0;
  }
  .collapsible-content[hidden] { display: block !important; }
  .collapsible-content-inner {
    padding: 1rem;
    margin-top: 0.5rem;
    background: #f0eeeb;
  }
</style>
<div data-slot="collapsible" class="group">
  <button
    data-slot="collapsible-trigger"
    class="px-4 py-2 bg-transparent border border-dashed border-gray-300
           cursor-pointer flex items-center gap-2 hover:bg-code-bg
           before:content-['▶'] before:text-[0.7rem]
           before:transition-transform before:duration-200
           aria-expanded:before:rotate-90"
  >
    Show more details
  </button>
  <div
    class="grid grid-rows-[0fr] transition-[grid-template-rows] duration-300
           group-data-[state=open]:grid-rows-[1fr]"
  >
    <div data-slot="collapsible-content" class="overflow-hidden min-h-0">
      <div class="p-4 mt-2 bg-code-bg">
        <p>Collapsible is the simplest component — just a trigger and content.</p>
        <p class="mt-2">Perfect for FAQ items, collapsible sections, or any show/hide pattern.</p>
      </div>
    </div>
  </div>
</div>

Tooltip

↓ sides: top, bottom, left, right (+ warm-up behavior)
Side: top (default)
Side: bottom
Side: left
Side: right
Side: top (default)
Side: bottom
Side: left
Side: right

Side via data-side attribute or JS option. Hover across quickly — warm-up skips delay for adjacent tooltips.

View source
<div data-slot="tooltip" class="tooltip-root">
  <button data-slot="tooltip-trigger" class="tooltip-trigger-btn">Hover me</button>
  <div data-slot="tooltip-content" data-side="top" class="tooltip-content">
    Tooltip content
  </div>
</div>

<style>
  .tooltip-root { display: inline-block; }
  .tooltip-trigger-btn {
    padding: 0.5rem 1rem;
    background: none;
    border: 1px dashed #ccc;
    cursor: help;
  }
  .tooltip-content {
    position: absolute;
    background: #1a1a1a;
    color: #faf9f7;
    padding: 0.5rem 0.75rem;
    font-size: 0.85rem;
    white-space: nowrap;
    z-index: 50;
    /* Hidden by default */
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transform-origin: center;
    --tooltip-slide-x: 0px;
    --tooltip-slide-y: -4px;
    /* Visibility delays hiding until after fade */
    transition: opacity 0.15s, visibility 0s linear 0.15s;
  }
  .tooltip-content[data-side="top"] { --tooltip-slide-y: 4px; }
  .tooltip-content[data-side="bottom"] { --tooltip-slide-y: -4px; }
  .tooltip-content[data-side="left"] { --tooltip-slide-x: 4px; --tooltip-slide-y: 0px; }
  .tooltip-content[data-side="right"] { --tooltip-slide-x: -4px; --tooltip-slide-y: 0px; }
  /* Open state */
  .tooltip-content[data-open] {
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    transition-delay: 0s;
    animation: tooltip-in 140ms cubic-bezier(0.16, 1, 0.3, 1);
  }
  .tooltip-content[data-closed] {
    pointer-events: none;
    visibility: visible;
    animation: tooltip-out 100ms ease-in forwards;
  }
  .tooltip-content[data-instant] {
    transition: none;
    animation: none;
  }
  @keyframes tooltip-in {
    from {
      opacity: 0;
      transform: translate3d(var(--tooltip-slide-x), var(--tooltip-slide-y), 0) scale(0.98);
    }
    to {
      opacity: 1;
      transform: translate3d(0, 0, 0) scale(1);
    }
  }
  @keyframes tooltip-out {
    from {
      opacity: 1;
      transform: translate3d(0, 0, 0) scale(1);
    }
    to {
      opacity: 0;
      transform: translate3d(var(--tooltip-slide-x), var(--tooltip-slide-y), 0) scale(0.98);
    }
  }
  /* Arrow */
  .tooltip-content::after {
    content: "";
    position: absolute;
    border: 6px solid transparent;
  }
  .tooltip-content[data-side="top"]::after {
    top: 100%;
    left: 50%;
    transform: translateX(-50%);
    border-top-color: #1a1a1a;
  }
</style>
<div data-slot="tooltip" class="inline-block">
  <button
    data-slot="tooltip-trigger"
    class="px-4 py-2 bg-transparent border border-dashed border-gray-300
           cursor-help underline decoration-dotted underline-offset-2"
  >
    Hover me
  </button>
  <div
    data-slot="tooltip-content"
    data-side="top"
    class="absolute
           bg-gray-900 text-white px-3 py-2 text-sm whitespace-nowrap z-50
           opacity-0 pointer-events-none transition-opacity duration-150
           data-[open]:opacity-100 data-[open]:pointer-events-auto
           data-[instant]:transition-none data-[instant]:[animation:none]
           after:content-[''] after:absolute after:top-full after:left-1/2
           after:-translate-x-1/2 after:border-[6px] after:border-transparent
           after:border-t-gray-900"
  >
    Tooltip content
  </div>
</div>

Popover

↓ interactive
View source
<div data-slot="popover" class="popover-root">
  <button data-slot="popover-trigger" class="popover-trigger-btn">Open Popover</button>
  <div data-slot="popover-content" data-side="bottom" data-align="center" class="popover-content" hidden>
    <div class="popover-title">Popover Panel</div>
    <p class="popover-text">
      Unlike tooltips, popovers stay open until dismissed.
      Click outside or press Escape to close.
    </p>
    <button data-slot="popover-close" class="popover-close-btn">Got it</button>
  </div>
</div>

<style>
  .popover-root { display: inline-block; }
  .popover-trigger-btn {
    padding: 0.5rem 1rem;
    background: #1a1a1a;
    color: #faf9f7;
    border: none;
    cursor: pointer;
  }
  .popover-content {
    position: fixed;
    background: #faf9f7;
    border: 1px solid #ccc;
    padding: 1rem;
    width: min(20rem, calc(100vw - 2rem));
    z-index: 50;
    transform-origin: var(--transform-origin, center);
    --popover-slide-x: 0px;
    --popover-slide-y: -4px;
  }
  .popover-content[data-side="top"] {
    --popover-slide-y: 4px;
  }
  .popover-content[data-side="bottom"] {
    --popover-slide-y: -4px;
  }
  .popover-content[data-side="left"] {
    --popover-slide-x: 4px;
    --popover-slide-y: 0px;
  }
  .popover-content[data-side="right"] {
    --popover-slide-x: -4px;
    --popover-slide-y: 0px;
  }
  .popover-content[data-open] {
    animation: popover-in 160ms cubic-bezier(0.16, 1, 0.3, 1);
  }
  .popover-content[data-closed] {
    pointer-events: none;
    animation: popover-out 120ms ease-in forwards;
  }
  @keyframes popover-in {
    from {
      opacity: 0;
      scale: 0.96;
      translate: var(--popover-slide-x) var(--popover-slide-y);
    }
    to {
      opacity: 1;
      scale: 1;
      translate: 0 0;
    }
  }
  @keyframes popover-out {
    from {
      opacity: 1;
      scale: 1;
      translate: 0 0;
    }
    to {
      opacity: 0;
      scale: 0.96;
      translate: var(--popover-slide-x) var(--popover-slide-y);
    }
  }
  .popover-title { font-weight: 700; margin-bottom: 0.5rem; }
  .popover-text { color: #666; font-size: 0.9rem; margin-bottom: 0.75rem; }
  .popover-close-btn {
    padding: 0.3rem 0.6rem;
    background: none;
    border: 1px solid #ccc;
    cursor: pointer;
  }
</style>
<div data-slot="popover" class="relative inline-block">
  <button
    data-slot="popover-trigger"
    class="px-4 py-2 bg-gray-900 text-white border-none cursor-pointer
           hover:opacity-90 transition-opacity"
  >
    Open Popover
  </button>
  <div
    data-slot="popover-content"
    data-side="bottom"
    data-align="center"
    class="fixed bg-white border border-gray-300
           p-4 w-80 max-w-[calc(100vw-2rem)] z-50 rounded-lg shadow-lg"
    hidden
  >
    <div class="font-bold mb-2 mt-0">Popover Panel</div>
    <p class="text-gray-500 text-sm mb-3">
      Unlike tooltips, popovers stay open until dismissed.
      Click outside or press Escape to close.
    </p>
    <button
      data-slot="popover-close"
      class="px-2.5 py-1 bg-transparent border border-gray-300
             cursor-pointer hover:bg-code-bg transition-colors"
    >
      Got it
    </button>
  </div>
</div>

/* State-based animation (works with presence lifecycle) */
[data-slot="popover-content"] {
  transform-origin: var(--transform-origin, center);
  --popover-slide-x: 0px;
  --popover-slide-y: -4px;
}
[data-slot="popover-content"][data-side="top"] {
  --popover-slide-y: 4px;
}
[data-slot="popover-content"][data-side="bottom"] {
  --popover-slide-y: -4px;
}
[data-slot="popover-content"][data-side="left"] {
  --popover-slide-x: 4px;
  --popover-slide-y: 0px;
}
[data-slot="popover-content"][data-side="right"] {
  --popover-slide-x: -4px;
  --popover-slide-y: 0px;
}
[data-slot="popover-content"][data-open] {
  animation: popover-in 160ms cubic-bezier(0.16, 1, 0.3, 1);
}
[data-slot="popover-content"][data-closed] {
  pointer-events: none;
  animation: popover-out 120ms ease-in forwards;
}
@keyframes popover-in {
  from {
    opacity: 0;
    scale: 0.96;
    translate: var(--popover-slide-x) var(--popover-slide-y);
  }
  to {
    opacity: 1;
    scale: 1;
    translate: 0 0;
  }
}
@keyframes popover-out {
  from {
    opacity: 1;
    scale: 1;
    translate: 0 0;
  }
  to {
    opacity: 0;
    scale: 0.96;
    translate: var(--popover-slide-x) var(--popover-slide-y);
  }
}

Hover Card

↓ base-ui style delays: data-delay + data-close-delay
View source
<div data-slot="hover-card" data-delay="180" data-close-delay="120" class="hover-card-root">
  <button data-slot="hover-card-trigger" class="hover-card-trigger-btn">@data-slot</button>
  <div data-slot="hover-card-content" data-side="bottom" data-align="start" class="hover-card-content" hidden>
    <div class="hover-card-head">
      <div class="hover-card-avatar">DS</div>
      <div>
        <p class="hover-card-title">data-slot</p>
        <p class="hover-card-handle">@data-slot</p>
      </div>
    </div>
    <p class="hover-card-text">Headless UI components for vanilla JavaScript.</p>
    <p class="hover-card-meta">Hover/focus to preview, leave to close.</p>
  </div>
</div>

<style>
  .hover-card-root { display: inline-block; }
  .hover-card-trigger-btn {
    padding: 0.5rem 0.875rem;
    background: none;
    border: 1px dashed #ccc;
    cursor: pointer;
    font-weight: 600;
  }
  .hover-card-content {
    position: fixed;
    width: min(18rem, calc(100vw - 2rem));
    background: #faf9f7;
    border: 1px solid #ccc;
    border-radius: 0.65rem;
    padding: 0.75rem;
    z-index: 50;
    transform-origin: var(--transform-origin, center);
    --hover-card-slide-x: 0px;
    --hover-card-slide-y: -4px;
  }
  .hover-card-content[data-side="top"] { --hover-card-slide-y: 4px; }
  .hover-card-content[data-side="bottom"] { --hover-card-slide-y: -4px; }
  .hover-card-content[data-side="left"] {
    --hover-card-slide-x: 4px;
    --hover-card-slide-y: 0px;
  }
  .hover-card-content[data-side="right"] {
    --hover-card-slide-x: -4px;
    --hover-card-slide-y: 0px;
  }
  .hover-card-content[data-open] {
    animation: hover-card-in 160ms cubic-bezier(0.16, 1, 0.3, 1);
  }
  .hover-card-content[data-closed] {
    pointer-events: none;
    animation: hover-card-out 120ms ease-in forwards;
  }
  .hover-card-content[data-instant] {
    transition: none;
    animation: none;
  }
  @keyframes hover-card-in {
    from {
      opacity: 0;
      scale: 0.96;
      translate: var(--hover-card-slide-x) var(--hover-card-slide-y);
    }
    to {
      opacity: 1;
      scale: 1;
      translate: 0 0;
    }
  }
  @keyframes hover-card-out {
    from {
      opacity: 1;
      scale: 1;
      translate: 0 0;
    }
    to {
      opacity: 0;
      scale: 0.96;
      translate: var(--hover-card-slide-x) var(--hover-card-slide-y);
    }
  }
  .hover-card-head {
    display: flex;
    align-items: center;
    gap: 0.625rem;
  }
  .hover-card-avatar {
    width: 2rem;
    height: 2rem;
    border-radius: 9999px;
    display: grid;
    place-items: center;
    background: #1a1a1a;
    color: #faf9f7;
    font-size: 0.75rem;
    font-weight: 700;
  }
  .hover-card-title {
    font-weight: 700;
    margin: 0;
    line-height: 1.15;
  }
  .hover-card-handle {
    margin: 0;
    color: #666;
    font-size: 0.82rem;
  }
  .hover-card-text {
    margin: 0.7rem 0 0;
    font-size: 0.9rem;
  }
  .hover-card-meta {
    margin: 0.35rem 0 0;
    font-size: 0.78rem;
    color: #666;
  }
</style>
<div data-slot="hover-card" data-delay="180" data-close-delay="120" class="inline-block">
  <button
    data-slot="hover-card-trigger"
    class="px-3.5 py-2 bg-transparent border border-dashed border-gray-300
           cursor-pointer font-semibold hover:bg-code-bg transition-colors"
  >
    @data-slot
  </button>

  <div
    data-slot="hover-card-content"
    data-side="bottom"
    data-align="start"
    class="fixed w-72 max-w-[calc(100vw-2rem)] bg-white border border-gray-300
           rounded-xl p-3 z-50 shadow-lg"
    hidden
  >
    <div class="flex items-center gap-2.5">
      <div class="w-8 h-8 rounded-full grid place-items-center bg-gray-900 text-white text-xs font-bold">DS</div>
      <div>
        <p class="font-bold leading-tight">data-slot</p>
        <p class="text-gray-500 text-xs">@data-slot</p>
      </div>
    </div>
    <p class="mt-3 text-sm">Headless UI components for vanilla JavaScript.</p>
    <p class="mt-1 text-xs text-gray-500">Hover/focus to preview, leave to close.</p>
  </div>
</div>

/* Animation hooks */
[data-slot="hover-card-content"] {
  transform-origin: var(--transform-origin, center);
  --hover-card-slide-x: 0px;
  --hover-card-slide-y: -4px;
}
[data-slot="hover-card-content"][data-side="top"] {
  --hover-card-slide-y: 4px;
}
[data-slot="hover-card-content"][data-side="bottom"] {
  --hover-card-slide-y: -4px;
}
[data-slot="hover-card-content"][data-side="left"] {
  --hover-card-slide-x: 4px;
  --hover-card-slide-y: 0px;
}
[data-slot="hover-card-content"][data-side="right"] {
  --hover-card-slide-x: -4px;
  --hover-card-slide-y: 0px;
}
[data-slot="hover-card-content"][data-open] {
  animation: hover-card-in 160ms cubic-bezier(0.16, 1, 0.3, 1);
}
[data-slot="hover-card-content"][data-closed] {
  pointer-events: none;
  animation: hover-card-out 120ms ease-in forwards;
}
[data-slot="hover-card-content"][data-instant] {
  transition: none;
  animation: none;
}

Navigation Menu

↓ mega menu with directional animations

Hover between items — content slides with direction. Viewport animates smoothly.

CSS variables: --viewport-width, --viewport-height, --motion-direction.

Use data-align="start|center|end" on items to control alignment.

View source
<nav data-slot="navigation-menu" class="nav-menu">
  <ul data-slot="navigation-menu-list" class="nav-menu-list">
    <li data-slot="navigation-menu-item" data-value="products" class="nav-menu-item">
      <button data-slot="navigation-menu-trigger" class="nav-menu-trigger">Products</button>
      <div data-slot="navigation-menu-content" class="nav-menu-content" hidden>
        <div class="nav-menu-grid">
          <a href="#" class="nav-menu-link">
            <div class="nav-menu-link-title">Analytics</div>
            <div class="nav-menu-link-desc">Real-time metrics</div>
          </a>
          <!-- More links... -->
        </div>
      </div>
    </li>
    <!-- More items... -->
    <div data-slot="navigation-menu-indicator" class="nav-menu-indicator"></div>
  </ul>
  <div data-slot="navigation-menu-viewport" class="nav-menu-viewport"></div>
</nav>

<style>
  .nav-menu { position: relative; }
  .nav-menu-list { display: flex; list-style: none; position: relative; }
  .nav-menu-trigger {
    padding: 0.6rem 1rem;
    background: none;
    border: none;
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: 0.35rem;
  }
  .nav-menu-trigger::after { content: ""; font-size: 0.6rem; }
  .nav-menu-trigger[data-state="open"]::after { transform: rotate(180deg); }
  .nav-menu-viewport {
    position: absolute;
    top: 0;
    left: 0;
    transform: translate3d(0, 0, 0);
    background: #faf9f7;
    border-radius: 8px;
    box-shadow: 0 4px 24px rgba(0,0,0,0.12);
    width: var(--viewport-width, 0);
    height: var(--viewport-height, 0);
    transition: transform 0.25s, width 0.25s, height 0.25s;
  }
  .nav-menu-content { position: absolute; top: 100%; padding: 1.5rem 1rem; }
  .nav-menu-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.75rem; width: 380px; }
</style>
<nav data-slot="navigation-menu" class="relative">
  <ul data-slot="navigation-menu-list" class="flex relative list-none">
    <li data-slot="navigation-menu-item" data-value="products" class="static">
      <button
        data-slot="navigation-menu-trigger"
        class="px-4 py-2.5 bg-transparent border-none cursor-pointer
               flex items-center gap-1.5 transition-colors relative z-1
               hover:text-accent data-[state=open]:text-accent
               after:content-['▼'] after:text-[0.6rem]
               after:transition-transform after:duration-200
               data-[state=open]:after:rotate-180"
      >
        Products
      </button>
      <div
        data-slot="navigation-menu-content"
        class="absolute top-full pt-6 px-4 pb-0
               data-motion[from-right]:animate-slide-in-from-right-4 data-motion[from-left]:animate-slide-in-from-left-4
               data-[state=inactive]:opacity-0 data-[state=inactive]:pointer-events-none
               data-[state=inactive]:invisible data-[state=active]:visible"
        hidden
      >
        <div class="grid grid-cols-2 gap-3 w-[380px]">
          <a href="#" class="block p-3 rounded transition-colors hover:bg-code-bg text-inherit">
            <div class="mb-1 font-medium">Analytics</div>
            <div class="text-sm text-muted">Real-time metrics</div>
          </a>
          <!-- More links... -->
        </div>
      </div>
    </li>
    <!-- More items... -->
    <div
      data-slot="navigation-menu-indicator"
      class="absolute bg-code-bg z-0 rounded-md transition-all duration-150
             pointer-events-none opacity-0 data-[state=visible]:opacity-100"
      style="left: var(--indicator-left, 0); top: var(--indicator-top, 0);
             width: var(--indicator-width, 0); height: var(--indicator-height, 0);"
    ></div>
  </ul>
  <div
    data-slot="navigation-menu-viewport"
    class="absolute top-0 left-0 bg-bg rounded-lg shadow-lg
           overflow-hidden mt-2 opacity-0 pointer-events-none
           data-[state=open]:opacity-100 data-[state=open]:pointer-events-auto"
    style="width: var(--viewport-width, 0); height: var(--viewport-height, 0);"
  ></div>
</nav>

/* CSS needed for animations (viewport size + content direction) */
[data-slot="navigation-menu-viewport"] {
  transition: transform 0.25s cubic-bezier(0.32, 0.72, 0, 1),
              width 0.25s cubic-bezier(0.32, 0.72, 0, 1),
              height 0.25s cubic-bezier(0.32, 0.72, 0, 1),
              opacity 0.15s ease;
  will-change: transform, width, height;
}
[data-slot="navigation-menu-viewport"][data-instant] {
  transition: opacity 0.15s ease;
}
[data-slot="navigation-menu-content"][data-motion="from-right"] {
  animation: slideFromRight 0.25s ease;
}
[data-slot="navigation-menu-content"][data-motion="from-left"] {
  animation: slideFromLeft 0.25s ease;
}

Dropdown Menu

↓ interactive
View source
<div data-slot="dropdown-menu" data-align="center" class="dropdown-root">
  <button data-slot="dropdown-menu-trigger" class="dropdown-trigger">
    Actions ▼
  </button>
  <div data-slot="dropdown-menu-content" class="dropdown-content" hidden>
    <div data-slot="dropdown-menu-group">
      <div data-slot="dropdown-menu-label" class="dropdown-label">Edit</div>
      <button data-slot="dropdown-menu-item" data-value="cut" class="dropdown-item">
        Cut
        <span data-slot="dropdown-menu-shortcut" class="dropdown-shortcut">⌘X</span>
      </button>
      <button data-slot="dropdown-menu-item" data-value="copy" class="dropdown-item">
        Copy
        <span data-slot="dropdown-menu-shortcut" class="dropdown-shortcut">⌘C</span>
      </button>
      <button data-slot="dropdown-menu-item" data-value="paste" class="dropdown-item">
        Paste
        <span data-slot="dropdown-menu-shortcut" class="dropdown-shortcut">⌘V</span>
      </button>
    </div>
    <div data-slot="dropdown-menu-separator" class="dropdown-separator"></div>
    <button data-slot="dropdown-menu-item" data-value="delete" data-variant="destructive" class="dropdown-item destructive">
      Delete
    </button>
    <button data-slot="dropdown-menu-item" data-disabled class="dropdown-item disabled">
      Archive (coming soon)
    </button>
  </div>
</div>

<style>
  .dropdown-root { position: relative; display: inline-block; }
  .dropdown-trigger {
    padding: 0.5rem 1rem;
    background: #1a1a1a;
    color: #faf9f7;
    border: none;
    cursor: pointer;
  }
  .dropdown-content {
    position: absolute;
    top: 100%;
    left: 0;
    margin-top: 0.25rem;
    background: #faf9f7;
    border: 1px solid #ccc;
    min-width: 180px;
    padding: 0.25rem;
    z-index: 50;
  }
  .dropdown-label {
    padding: 0.375rem 0.5rem;
    font-size: 0.75rem;
    font-weight: 600;
    color: #888;
  }
  .dropdown-item {
    display: flex;
    align-items: center;
    width: 100%;
    padding: 0.375rem 0.5rem;
    border: none;
    background: none;
    cursor: pointer;
    font-size: 0.875rem;
    text-align: left;
  }
  .dropdown-item[data-highlighted] {
    background: #e5e5e5;
  }
  .dropdown-item.destructive { color: #dc2626; }
  .dropdown-item.destructive[data-highlighted] {
    background: #fef2f2;
  }
  .dropdown-item.disabled {
    color: #aaa;
    cursor: not-allowed;
  }
  .dropdown-shortcut {
    margin-left: auto;
    font-size: 0.75rem;
    color: #888;
  }
  .dropdown-separator {
    height: 1px;
    background: #ddd;
    margin: 0.25rem 0;
  }
</style>
<div data-slot="dropdown-menu" class="relative inline-block">
  <button
    data-slot="dropdown-menu-trigger"
    class="px-4 py-2 bg-gray-900 text-white border-none cursor-pointer
           hover:opacity-90 transition-opacity"
  >
    Actions ▼
  </button>
  <div
    data-slot="dropdown-menu-content"
    class="absolute top-full left-0 mt-1 bg-white border border-gray-300
           min-w-[180px] p-1 z-50 rounded-lg shadow-lg"
    hidden
  >
    <div data-slot="dropdown-menu-group">
      <div
        data-slot="dropdown-menu-label"
        class="px-2 py-1.5 text-xs font-semibold text-gray-400"
      >
        Edit
      </div>
      <button
        data-slot="dropdown-menu-item"
        data-value="cut"
        class="flex items-center w-full px-2 py-1.5 text-sm text-left rounded
               data-[highlighted]:bg-gray-100"
      >
        Cut
        <span data-slot="dropdown-menu-shortcut" class="ml-auto text-xs text-gray-400">⌘X</span>
      </button>
      <button
        data-slot="dropdown-menu-item"
        data-value="copy"
        class="flex items-center w-full px-2 py-1.5 text-sm text-left rounded
               data-[highlighted]:bg-gray-100"
      >
        Copy
        <span data-slot="dropdown-menu-shortcut" class="ml-auto text-xs text-gray-400">⌘C</span>
      </button>
      <button
        data-slot="dropdown-menu-item"
        data-value="paste"
        class="flex items-center w-full px-2 py-1.5 text-sm text-left rounded
               data-[highlighted]:bg-gray-100"
      >
        Paste
        <span data-slot="dropdown-menu-shortcut" class="ml-auto text-xs text-gray-400">⌘V</span>
      </button>
    </div>
    <div data-slot="dropdown-menu-separator" class="h-px bg-gray-200 my-1"></div>
    <button
      data-slot="dropdown-menu-item"
      data-value="delete"
      data-variant="destructive"
      class="flex items-center w-full px-2 py-1.5 text-sm text-left rounded
             text-red-600 data-[highlighted]:bg-red-50"
    >
      Delete
    </button>
    <button
      data-slot="dropdown-menu-item"
      data-disabled
      class="flex items-center w-full px-2 py-1.5 text-sm text-left rounded
             text-gray-300 cursor-not-allowed"
    >
      Archive (coming soon)
    </button>
  </div>
</div>

Select

↓ interactive
View source
<label for="fruit-select" class="select-field-label">Fruit</label>
<div data-slot="select" data-placeholder="Select a fruit..." class="select-root">
  <button data-slot="select-trigger" id="fruit-select" class="select-trigger">
    <span data-slot="select-value"></span>
    <span class="select-icon"></span>
  </button>
  <div data-slot="select-content" class="select-content" hidden>
    <div data-slot="select-group">
      <div data-slot="select-label" class="select-label">Fruits</div>
      <div data-slot="select-item" data-value="apple" data-label="Apple" class="select-item">
        Apple<span class="select-check"></span>
      </div>
      <div data-slot="select-item" data-value="banana" data-label="Banana" class="select-item">
        Banana<span class="select-check"></span>
      </div>
      <div data-slot="select-item" data-value="orange" data-label="Orange" class="select-item">
        Orange<span class="select-check"></span>
      </div>
      <div data-slot="select-item" data-value="mango" data-label="Mango" class="select-item">
        Mango<span class="select-check"></span>
      </div>
      <div data-slot="select-item" data-value="grape" data-label="Grape" class="select-item">
        Grape<span class="select-check"></span>
      </div>
      <div data-slot="select-item" data-value="pear" data-label="Pear" class="select-item">
        Pear<span class="select-check"></span>
      </div>
      <div data-slot="select-item" data-value="peach" data-label="Peach" class="select-item">
        Peach<span class="select-check"></span>
      </div>
      <div data-slot="select-item" data-value="pineapple" data-label="Pineapple" class="select-item">
        Pineapple<span class="select-check"></span>
      </div>
    </div>
    <div data-slot="select-separator" class="select-separator"></div>
    <div data-slot="select-group">
      <div data-slot="select-label" class="select-label">Vegetables</div>
      <div data-slot="select-item" data-value="carrot" data-label="Carrot" class="select-item">
        Carrot<span class="select-check"></span>
      </div>
      <div data-slot="select-item" data-value="broccoli" data-label="Broccoli" class="select-item">
        Broccoli<span class="select-check"></span>
      </div>
      <div data-slot="select-item" data-value="cucumber" data-label="Cucumber" class="select-item">
        Cucumber<span class="select-check"></span>
      </div>
      <div data-slot="select-item" data-value="tomato" data-label="Tomato" class="select-item">
        Tomato<span class="select-check"></span>
      </div>
      <div data-slot="select-item" data-value="pepper" data-label="Bell Pepper" class="select-item">
        Bell Pepper<span class="select-check"></span>
      </div>
      <div data-slot="select-item" data-value="zucchini" data-label="Zucchini" class="select-item">
        Zucchini<span class="select-check"></span>
      </div>
      <div data-slot="select-item" data-value="spinach" data-label="Spinach" data-disabled class="select-item disabled">
        Spinach (out of stock)<span class="select-check"></span>
      </div>
    </div>
  </div>
</div>

<style>
  .select-root { position: relative; display: inline-block; }
  .select-field-label {
    display: block;
    font-size: 0.875rem;
    font-weight: 500;
    color: #333;
    margin-bottom: 0.25rem;
  }
  .select-trigger {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 1rem;
    min-width: 180px;
    background: #1a1a1a;
    color: #faf9f7;
    border: none;
    cursor: pointer;
  }
  .select-trigger[data-placeholder] [data-slot="select-value"] {
    color: #888;
  }
  .select-icon {
    margin-left: auto;
    font-size: 0.75rem;
  }
  .select-content {
    background: #faf9f7;
    border: 1px solid #ccc;
    min-width: 180px;
    max-height: 220px;
    overflow-y: auto;
    padding: 0.25rem;
    z-index: 50;
  }
  .select-label {
    padding: 0.375rem 0.5rem;
    font-size: 0.75rem;
    font-weight: 600;
    color: #888;
  }
  .select-item {
    display: flex;
    align-items: center;
    width: 100%;
    padding: 0.375rem 0.5rem;
    cursor: pointer;
    font-size: 0.875rem;
  }
  .select-check {
    margin-left: auto;
    visibility: hidden;
  }
  .select-item[data-selected] .select-check {
    visibility: visible;
  }
  .select-item[data-highlighted] {
    background: #e5e5e5;
  }
  .select-item.disabled {
    color: #aaa;
    cursor: not-allowed;
  }
  .select-separator {
    height: 1px;
    background: #ddd;
    margin: 0.25rem 0;
  }
</style>
<label for="fruit-select" class="block mb-1 text-sm font-medium text-gray-700">Fruit</label>
<div data-slot="select" data-placeholder="Select a fruit..." class="inline-block relative">
  <button
    data-slot="select-trigger"
    id="fruit-select"
    class="flex items-center gap-2 px-4 py-2 min-w-[180px] bg-gray-900 text-white
           border-none cursor-pointer hover:opacity-90 transition-opacity"
  >
    <span data-slot="select-value" class="data-[placeholder]:text-gray-400"></span>
    <span class="ml-auto text-xs"></span>
  </button>
  <div
    data-slot="select-content"
    class="absolute top-full left-0 mt-1 bg-white border border-gray-300
           min-w-[180px] max-h-[220px] overflow-y-auto p-1 z-50 rounded-lg shadow-lg"
    hidden
  >
    <div data-slot="select-group">
      <div data-slot="select-label" class="px-2 py-1.5 text-xs font-semibold text-gray-400">
        Fruits
      </div>
      <div
        data-slot="select-item"
        data-value="apple"
        data-label="Apple"
        class="group flex items-center w-full px-2 py-1.5 text-sm rounded cursor-pointer
               data-[highlighted]:bg-gray-100"
      >
        Apple<span class="ml-auto invisible group-data-[selected]:visible"></span>
      </div>
      <div
        data-slot="select-item"
        data-value="banana"
        data-label="Banana"
        class="group flex items-center w-full px-2 py-1.5 text-sm rounded cursor-pointer
               data-[highlighted]:bg-gray-100"
      >
        Banana<span class="ml-auto invisible group-data-[selected]:visible"></span>
      </div>
      <div
        data-slot="select-item"
        data-value="orange"
        data-label="Orange"
        class="group flex items-center w-full px-2 py-1.5 text-sm rounded cursor-pointer
               data-[highlighted]:bg-gray-100"
      >
        Orange<span class="ml-auto invisible group-data-[selected]:visible"></span>
      </div>
      <div
        data-slot="select-item"
        data-value="mango"
        data-label="Mango"
        class="group flex items-center w-full px-2 py-1.5 text-sm rounded cursor-pointer
               data-[highlighted]:bg-gray-100"
      >
        Mango<span class="ml-auto invisible group-data-[selected]:visible"></span>
      </div>
      <div
        data-slot="select-item"
        data-value="grape"
        data-label="Grape"
        class="group flex items-center w-full px-2 py-1.5 text-sm rounded cursor-pointer
               data-[highlighted]:bg-gray-100"
      >
        Grape<span class="ml-auto invisible group-data-[selected]:visible"></span>
      </div>
      <div
        data-slot="select-item"
        data-value="pear"
        data-label="Pear"
        class="group flex items-center w-full px-2 py-1.5 text-sm rounded cursor-pointer
               data-[highlighted]:bg-gray-100"
      >
        Pear<span class="ml-auto invisible group-data-[selected]:visible"></span>
      </div>
      <div
        data-slot="select-item"
        data-value="peach"
        data-label="Peach"
        class="group flex items-center w-full px-2 py-1.5 text-sm rounded cursor-pointer
               data-[highlighted]:bg-gray-100"
      >
        Peach<span class="ml-auto invisible group-data-[selected]:visible"></span>
      </div>
      <div
        data-slot="select-item"
        data-value="pineapple"
        data-label="Pineapple"
        class="group flex items-center w-full px-2 py-1.5 text-sm rounded cursor-pointer
               data-[highlighted]:bg-gray-100"
      >
        Pineapple<span class="ml-auto invisible group-data-[selected]:visible"></span>
      </div>
    </div>
    <div data-slot="select-separator" class="my-1 h-px bg-gray-200"></div>
    <div data-slot="select-group">
      <div data-slot="select-label" class="px-2 py-1.5 text-xs font-semibold text-gray-400">
        Vegetables
      </div>
      <div
        data-slot="select-item"
        data-value="carrot"
        data-label="Carrot"
        class="group flex items-center w-full px-2 py-1.5 text-sm rounded cursor-pointer
               data-[highlighted]:bg-gray-100"
      >
        Carrot<span class="ml-auto invisible group-data-[selected]:visible"></span>
      </div>
      <div
        data-slot="select-item"
        data-value="broccoli"
        data-label="Broccoli"
        class="group flex items-center w-full px-2 py-1.5 text-sm rounded cursor-pointer
               data-[highlighted]:bg-gray-100"
      >
        Broccoli<span class="ml-auto invisible group-data-[selected]:visible"></span>
      </div>
      <div
        data-slot="select-item"
        data-value="cucumber"
        data-label="Cucumber"
        class="group flex items-center w-full px-2 py-1.5 text-sm rounded cursor-pointer
               data-[highlighted]:bg-gray-100"
      >
        Cucumber<span class="ml-auto invisible group-data-[selected]:visible"></span>
      </div>
      <div
        data-slot="select-item"
        data-value="tomato"
        data-label="Tomato"
        class="group flex items-center w-full px-2 py-1.5 text-sm rounded cursor-pointer
               data-[highlighted]:bg-gray-100"
      >
        Tomato<span class="ml-auto invisible group-data-[selected]:visible"></span>
      </div>
      <div
        data-slot="select-item"
        data-value="pepper"
        data-label="Bell Pepper"
        class="group flex items-center w-full px-2 py-1.5 text-sm rounded cursor-pointer
               data-[highlighted]:bg-gray-100"
      >
        Bell Pepper<span class="ml-auto invisible group-data-[selected]:visible"></span>
      </div>
      <div
        data-slot="select-item"
        data-value="zucchini"
        data-label="Zucchini"
        class="group flex items-center w-full px-2 py-1.5 text-sm rounded cursor-pointer
               data-[highlighted]:bg-gray-100"
      >
        Zucchini<span class="ml-auto invisible group-data-[selected]:visible"></span>
      </div>
      <div
        data-slot="select-item"
        data-value="spinach"
        data-label="Spinach"
        data-disabled
        class="flex items-center px-2 py-1.5 w-full text-sm text-gray-300 rounded cursor-not-allowed group"
      >
        Spinach (out of stock)<span class="ml-auto invisible group-data-[selected]:visible"></span>
      </div>
    </div>
  </div>
</div>

Combobox

↓ interactive
View source
<label for="fruit-combo" class="combobox-field-label">Fruit</label>
<div data-slot="combobox" data-placeholder="Search fruits..." class="combobox-root">
  <div class="combobox-input-wrapper">
    <input data-slot="combobox-input" id="fruit-combo" class="combobox-input" />
    <button data-slot="combobox-trigger" class="combobox-trigger-btn"></button>
  </div>
  <div data-slot="combobox-content" class="combobox-content" hidden>
    <div data-slot="combobox-list">
      <div data-slot="combobox-empty" class="combobox-empty" hidden>No results found</div>
      <div data-slot="combobox-group">
        <div data-slot="combobox-label" class="combobox-label">Fruits</div>
        <div data-slot="combobox-item" data-value="apple" data-label="Apple" class="combobox-item">
          Apple<span class="combobox-check"></span>
        </div>
        <div data-slot="combobox-item" data-value="banana" data-label="Banana" class="combobox-item">
          Banana<span class="combobox-check"></span>
        </div>
        <div data-slot="combobox-item" data-value="orange" data-label="Orange" class="combobox-item">
          Orange<span class="combobox-check"></span>
        </div>
      </div>
      <div data-slot="combobox-separator" class="combobox-separator"></div>
      <div data-slot="combobox-group">
        <div data-slot="combobox-label" class="combobox-label">Vegetables</div>
        <div data-slot="combobox-item" data-value="carrot" data-label="Carrot" class="combobox-item">
          Carrot<span class="combobox-check"></span>
        </div>
        <div data-slot="combobox-item" data-value="broccoli" data-label="Broccoli" class="combobox-item">
          Broccoli<span class="combobox-check"></span>
        </div>
        <div data-slot="combobox-item" data-value="spinach" data-label="Spinach (out of stock)" data-disabled class="combobox-item disabled">
          Spinach (out of stock)<span class="combobox-check"></span>
        </div>
      </div>
    </div>
  </div>
</div>

<style>
  .combobox-root { display: inline-block; }
  .combobox-field-label {
    display: block;
    font-size: 0.875rem;
    font-weight: 500;
    color: #333;
    margin-bottom: 0.25rem;
  }
  .combobox-input-wrapper {
    display: flex;
    align-items: center;
    background: #1a1a1a;
    min-width: 220px;
  }
  .combobox-input {
    flex: 1;
    padding: 0.5rem 0.75rem;
    background: transparent;
    color: #faf9f7;
    border: none;
    outline: none;
    font-size: 0.875rem;
  }
  .combobox-input::placeholder { color: #888; }
  .combobox-trigger-btn {
    padding: 0.5rem;
    background: transparent;
    color: #888;
    border: none;
    cursor: pointer;
    font-size: 0.75rem;
  }
  .combobox-content {
    background: #faf9f7;
    border: 1px solid #ccc;
    min-width: 220px;
    max-height: 200px;
    overflow-y: auto;
    padding: 0.25rem;
    z-index: 50;
  }
  .combobox-label {
    padding: 0.375rem 0.5rem;
    font-size: 0.75rem;
    font-weight: 600;
    color: #888;
  }
  .combobox-item {
    display: flex;
    align-items: center;
    width: 100%;
    padding: 0.375rem 0.5rem;
    cursor: pointer;
    font-size: 0.875rem;
  }
  .combobox-check {
    margin-left: auto;
    visibility: hidden;
  }
  .combobox-item[data-selected] .combobox-check {
    visibility: visible;
  }
  .combobox-item[data-highlighted] {
    background: #e5e5e5;
  }
  .combobox-item.disabled {
    color: #aaa;
    cursor: not-allowed;
  }
  .combobox-empty {
    padding: 0.5rem;
    text-align: center;
    font-size: 0.875rem;
    color: #888;
  }
  .combobox-separator {
    height: 1px;
    background: #ddd;
    margin: 0.25rem 0;
  }
</style>
<label for="fruit-combo" class="block text-sm font-medium text-gray-700 mb-1">Fruit</label>
<div data-slot="combobox" data-placeholder="Search fruits..." class="relative inline-block">
  <div class="flex items-center bg-gray-900 min-w-[220px]">
    <input
      data-slot="combobox-input"
      id="fruit-combo"
      class="flex-1 px-3 py-2 bg-transparent text-white border-none outline-none text-sm
             placeholder:text-gray-400"
    />
    <button
      data-slot="combobox-trigger"
      class="px-2 py-2 bg-transparent text-gray-400 border-none cursor-pointer text-xs"
    ></button>
  </div>
  <div
    data-slot="combobox-content"
    class="bg-white border border-gray-300 min-w-[220px] max-h-[200px] overflow-y-auto
           p-1 z-50 rounded-lg shadow-lg"
    hidden
  >
    <div data-slot="combobox-list">
      <div data-slot="combobox-empty" class="py-2 text-center text-sm text-gray-400" hidden>
        No results found
      </div>
      <div data-slot="combobox-group">
        <div data-slot="combobox-label" class="px-2 py-1.5 text-xs font-semibold text-gray-400">
          Fruits
        </div>
        <div
          data-slot="combobox-item"
          data-value="apple"
          data-label="Apple"
          class="group flex items-center w-full px-2 py-1.5 text-sm rounded cursor-pointer
                 data-[highlighted]:bg-gray-100"
        >
          Apple<span class="ml-auto invisible group-data-[selected]:visible"></span>
        </div>
        <div
          data-slot="combobox-item"
          data-value="banana"
          data-label="Banana"
          class="group flex items-center w-full px-2 py-1.5 text-sm rounded cursor-pointer
                 data-[highlighted]:bg-gray-100"
        >
          Banana<span class="ml-auto invisible group-data-[selected]:visible"></span>
        </div>
        <div
          data-slot="combobox-item"
          data-value="orange"
          data-label="Orange"
          class="group flex items-center w-full px-2 py-1.5 text-sm rounded cursor-pointer
                 data-[highlighted]:bg-gray-100"
        >
          Orange<span class="ml-auto invisible group-data-[selected]:visible"></span>
        </div>
      </div>
      <div data-slot="combobox-separator" class="h-px bg-gray-200 my-1"></div>
      <div data-slot="combobox-group">
        <div data-slot="combobox-label" class="px-2 py-1.5 text-xs font-semibold text-gray-400">
          Vegetables
        </div>
        <div
          data-slot="combobox-item"
          data-value="carrot"
          data-label="Carrot"
          class="group flex items-center w-full px-2 py-1.5 text-sm rounded cursor-pointer
                 data-[highlighted]:bg-gray-100"
        >
          Carrot<span class="ml-auto invisible group-data-[selected]:visible"></span>
        </div>
        <div
          data-slot="combobox-item"
          data-value="broccoli"
          data-label="Broccoli"
          class="group flex items-center w-full px-2 py-1.5 text-sm rounded cursor-pointer
                 data-[highlighted]:bg-gray-100"
        >
          Broccoli<span class="ml-auto invisible group-data-[selected]:visible"></span>
        </div>
        <div
          data-slot="combobox-item"
          data-value="spinach"
          data-label="Spinach (out of stock)"
          data-disabled
          class="group flex items-center w-full px-2 py-1.5 text-sm rounded
                 text-gray-300 cursor-not-allowed"
        >
          Spinach (out of stock)<span class="ml-auto invisible group-data-[selected]:visible"></span>
        </div>
      </div>
    </div>
  </div>
</div>

Slider

↓ Basic Slider
View source
<div data-slot="slider" data-default-value="50" class="slider">
  <div class="slider-control">
    <div data-slot="slider-track" class="slider-track">
      <div data-slot="slider-range" class="slider-range"></div>
    </div>
    <div data-slot="slider-thumb" class="slider-thumb"></div>
  </div>
</div>

<style>
  .slider {
    position: relative;
    width: 100%;
    padding: 0.5rem 0;
  }
  .slider-control {
    position: relative;
    display: flex;
    align-items: center;
    width: 100%;
    height: 1.25rem;
  }
  .slider-track {
    position: relative;
    flex: 1;
    height: 4px;
    background: #e5e5e5;
    border-radius: 2px;
  }
  .slider-range {
    position: absolute;
    height: 100%;
    background: #1a1a1a;
    border-radius: 2px;
  }
  .slider-thumb {
    position: absolute;
    width: 1.25rem;
    height: 1.25rem;
    background: #fff;
    border: 2px solid #1a1a1a;
    border-radius: 50%;
    cursor: grab;
    transform: translateX(-50%);
  }
  .slider-thumb:focus {
    outline: none;
    box-shadow: 0 0 0 3px rgba(26, 26, 26, 0.2);
  }
  .slider-thumb[data-dragging] { cursor: grabbing; }
  .slider[data-disabled] .slider-thumb {
    cursor: not-allowed;
    opacity: 0.5;
  }
</style>
<div data-slot="slider" data-default-value="50" class="relative w-full py-2">
  <div class="relative flex items-center w-full h-5">
    <div data-slot="slider-track" class="relative flex-1 h-1 bg-gray-200 rounded">
      <div data-slot="slider-range" class="absolute h-full bg-gray-900 rounded"></div>
    </div>
    <div
      data-slot="slider-thumb"
      class="absolute w-5 h-5 bg-white border-2 border-gray-900 rounded-full cursor-grab
             -translate-x-1/2 focus:outline-none focus:ring-2 focus:ring-gray-900/20
             data-[dragging]:cursor-grabbing"
    ></div>
  </div>
</div>
↓ Range Slider
View source
<div data-slot="slider" data-default-value="25,75" class="slider">
  <div class="slider-control">
    <div data-slot="slider-track" class="slider-track">
      <div data-slot="slider-range" class="slider-range"></div>
    </div>
    <div data-slot="slider-thumb" class="slider-thumb"></div>
    <div data-slot="slider-thumb" class="slider-thumb"></div>
  </div>
</div>

<style>
  /* Same styles as basic slider */
</style>
<div data-slot="slider" data-default-value="25,75" class="relative w-full py-2">
  <div class="relative flex items-center w-full h-5">
    <div data-slot="slider-track" class="relative flex-1 h-1 bg-gray-200 rounded">
      <div data-slot="slider-range" class="absolute h-full bg-gray-900 rounded"></div>
    </div>
    <div
      data-slot="slider-thumb"
      class="absolute w-5 h-5 bg-white border-2 border-gray-900 rounded-full cursor-grab
             -translate-x-1/2 focus:outline-none focus:ring-2 focus:ring-gray-900/20
             data-[dragging]:cursor-grabbing"
    ></div>
    <div
      data-slot="slider-thumb"
      class="absolute w-5 h-5 bg-white border-2 border-gray-900 rounded-full cursor-grab
             -translate-x-1/2 focus:outline-none focus:ring-2 focus:ring-gray-900/20
             data-[dragging]:cursor-grabbing"
    ></div>
  </div>
</div>

Toggle

↓ interactive
View source
<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;
  }
  .toggle-btn {
    padding: 0.5rem;
    background: transparent;
    border: 1px solid #ccc;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: background 0.15s, border-color 0.15s;
  }
  .toggle-btn:hover {
    background: #f0eeeb;
  }
  .toggle-btn[data-state="on"] {
    background: #333;
    border-color: #333;
    color: white;
  }
</style>
<div class="flex gap-1">
  <button
    data-slot="toggle"
    class="p-2 bg-transparent border border-gray-300 cursor-pointer
           flex items-center justify-center transition-colors
           hover:bg-code-bg data-[state=on]:bg-gray-800
           data-[state=on]:border-gray-800 data-[state=on]:text-white"
  >
    <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-gray-300 cursor-pointer
           flex items-center justify-center transition-colors
           hover:bg-code-bg data-[state=on]:bg-gray-800
           data-[state=on]:border-gray-800 data-[state=on]:text-white"
  >
    <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-gray-300 cursor-pointer
           flex items-center justify-center transition-colors
           hover:bg-code-bg data-[state=on]:bg-gray-800
           data-[state=on]:border-gray-800 data-[state=on]:text-white"
  >
    <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>

Toggle Group

↓ interactive
View source
<!-- Single selection (alignment) -->
<div data-slot="toggle-group" data-default-value="center" class="toggle-group">
  <button data-slot="toggle-group-item" data-value="left" class="toggle-group-btn" aria-label="Align left">
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
      <line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="15" y2="12"/><line x1="3" y1="18" x2="18" y2="18"/>
    </svg>
  </button>
  <button data-slot="toggle-group-item" data-value="center" class="toggle-group-btn" aria-label="Align center">
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
      <line x1="3" y1="6" x2="21" y2="6"/><line x1="6" y1="12" x2="18" y2="12"/><line x1="4" y1="18" x2="20" y2="18"/>
    </svg>
  </button>
  <button data-slot="toggle-group-item" data-value="right" class="toggle-group-btn" aria-label="Align right">
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
      <line x1="3" y1="6" x2="21" y2="6"/><line x1="9" y1="12" x2="21" y2="12"/><line x1="6" y1="18" x2="21" y2="18"/>
    </svg>
  </button>
</div>

<!-- Multiple selection (text formatting) -->
<div data-slot="toggle-group" data-multiple data-default-value="bold" class="toggle-group" aria-label="Text formatting">
  <button data-slot="toggle-group-item" data-value="bold" class="toggle-group-btn" aria-label="Bold">
    <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-group-item" data-value="italic" class="toggle-group-btn" aria-label="Italic">
    <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-group-item" data-value="underline" class="toggle-group-btn" aria-label="Underline">
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
      <path d="M6 4v6a6 6 0 0 0 12 0V4"/><line x1="4" y1="20" x2="20" y2="20"/>
    </svg>
  </button>
</div>

<style>
  .toggle-group {
    display: inline-flex;
    gap: 0.25rem;
    margin-bottom: 0.75rem;
  }
  .toggle-group-btn {
    padding: 0.5rem;
    background: transparent;
    border: 1px solid #ccc;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: background 0.15s, border-color 0.15s;
  }
  .toggle-group-btn:hover {
    background: #f0eeeb;
  }
  .toggle-group-btn[data-state="on"] {
    background: #333;
    border-color: #333;
    color: white;
  }
</style>
<!-- Single selection (alignment) -->
<div data-slot="toggle-group" data-default-value="center" class="inline-flex gap-1 mb-3">
  <button
    data-slot="toggle-group-item"
    data-value="left"
    aria-label="Align left"
    class="p-2 bg-transparent border border-gray-300 cursor-pointer
           flex items-center justify-center transition-colors
           hover:bg-code-bg data-[state=on]:bg-gray-800
           data-[state=on]:border-gray-800 data-[state=on]:text-white"
  >
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
      <line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="15" y2="12"/><line x1="3" y1="18" x2="18" y2="18"/>
    </svg>
  </button>
  <button
    data-slot="toggle-group-item"
    data-value="center"
    aria-label="Align center"
    class="p-2 bg-transparent border border-gray-300 cursor-pointer
           flex items-center justify-center transition-colors
           hover:bg-code-bg data-[state=on]:bg-gray-800
           data-[state=on]:border-gray-800 data-[state=on]:text-white"
  >
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
      <line x1="3" y1="6" x2="21" y2="6"/><line x1="6" y1="12" x2="18" y2="12"/><line x1="4" y1="18" x2="20" y2="18"/>
    </svg>
  </button>
  <button
    data-slot="toggle-group-item"
    data-value="right"
    aria-label="Align right"
    class="p-2 bg-transparent border border-gray-300 cursor-pointer
           flex items-center justify-center transition-colors
           hover:bg-code-bg data-[state=on]:bg-gray-800
           data-[state=on]:border-gray-800 data-[state=on]:text-white"
  >
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
      <line x1="3" y1="6" x2="21" y2="6"/><line x1="9" y1="12" x2="21" y2="12"/><line x1="6" y1="18" x2="21" y2="18"/>
    </svg>
  </button>
</div>

<!-- Multiple selection (text formatting) -->
<div data-slot="toggle-group" data-multiple data-default-value="bold" class="inline-flex gap-1" aria-label="Text formatting">
  <button
    data-slot="toggle-group-item"
    data-value="bold"
    aria-label="Bold"
    class="p-2 bg-transparent border border-gray-300 cursor-pointer
           flex items-center justify-center transition-colors
           hover:bg-code-bg data-[state=on]:bg-gray-800
           data-[state=on]:border-gray-800 data-[state=on]:text-white"
  >
    <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-group-item"
    data-value="italic"
    aria-label="Italic"
    class="p-2 bg-transparent border border-gray-300 cursor-pointer
           flex items-center justify-center transition-colors
           hover:bg-code-bg data-[state=on]:bg-gray-800
           data-[state=on]:border-gray-800 data-[state=on]:text-white"
  >
    <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-group-item"
    data-value="underline"
    aria-label="Underline"
    class="p-2 bg-transparent border border-gray-300 cursor-pointer
           flex items-center justify-center transition-colors
           hover:bg-code-bg data-[state=on]:bg-gray-800
           data-[state=on]:border-gray-800 data-[state=on]:text-white"
  >
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
      <path d="M6 4v6a6 6 0 0 0 12 0V4"/><line x1="4" y1="20" x2="20" y2="20"/>
    </svg>
  </button>
</div>
~~~

STYLING

Components expose data-state and ARIA attributes for CSS hooks:

/* State-based styling */
[data-state="active"]   { ... }
[data-state="open"]     { ... }
[aria-expanded="true"] { ... }
[aria-selected="true"] { ... }

/* With Tailwind */
<button class="aria-selected:font-bold">Tab</button>

API

All components follow the same pattern:

// Auto-bind all instances
import { create } from "@data-slot/[component]";
const controllers = create(scope?);

// Create for specific element
import { createDialog } from "@data-slot/dialog";
const dialog = createDialog(element, options?);

// Common controller methods
controller.destroy(); // cleanup listeners