+----------------------------+--------+-----------------------------+ | 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 | +----------------------------+--------+-----------------------------+
Install only what you need:
bun add @data-slot/tabs
bun add @data-slot/dialog
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 Interactive examples using the library. Inspect the source for markup patterns.
<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" <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> --active-tab-left and --active-tab-width.
transition on transform and width.
Works with keyboard navigation too.
data-slot="tabs-indicator" inside your tabs-list.
The component sets CSS variables automatically.
<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> data-slot attributes to your markup and call create().
No framework, no virtual DOM, no build step required.
data-slot attributes to your markup and call create().
No framework, no virtual DOM, no build step required.
<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: true, multiple items can be open simultaneously.
multiple: true, multiple items can be open simultaneously.
<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> This dialog traps focus and can be closed with Escape or clicking outside.
This dialog traps focus and can be closed with Escape or clicking outside.
<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 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.
<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>
Side via data-side attribute or JS option. Hover across quickly — warm-up skips delay for adjacent tooltips.
<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> Unlike tooltips, popovers stay open until dismissed. Click outside or press Escape to close.
Unlike tooltips, popovers stay open until dismissed. Click outside or press Escape to close.
<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);
}
} data-slot
@data-slot
Headless UI components for vanilla JavaScript.
data-slot
@data-slot
Headless UI components for vanilla JavaScript.
Hover/focus to preview, leave to close.
<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;
} 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.
<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;
} <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> <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> <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> <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> <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> <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> <!-- 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>
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> 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