Quick start
headless UI components for vanilla JavaScript.
data-slot adds accessible behavior to your markup with a few attributes and a small JavaScript module. Bring your own styles, use your favorite framework, or use none at all.
Install a component
Install only the packages you need. Each component is available separately.
bun add @data-slot/tabsnpm install @data-slot/tabspnpm add @data-slot/tabsAssemble the markup
Use data-slot to identify each part. Related triggers and panels share a data-value.
<div data-slot="tabs" data-default-value="one">
<div data-slot="tabs-list" aria-label="Getting started">
<button data-slot="tabs-trigger" data-value="one">Overview</button>
<button data-slot="tabs-trigger" data-value="two">Details</button>
</div>
<div data-slot="tabs-content" data-value="one">Your overview.</div>
<div data-slot="tabs-content" data-value="two">Your details.</div>
</div>
Add the behavior
Import and call create() to make the tabs interactive:
import { create } from '@data-slot/tabs';
// Find and initialize all [data-slot="tabs"] elements.
create();
The tabs now respond to clicks and keyboard navigation.
To target a specific element and control its tabs from JavaScript, use createTabs() instead:
import { createTabs } from '@data-slot/tabs';
const element = document.querySelector('[data-slot="tabs"]');
const tabs = createTabs(element);
tabs.select('two'); // Show the Details panel.
Call tabs.destroy() before removing that element to clean up its listeners.
Run either setup after the markup is in the document. Use a bundler such as Vite to resolve package imports, and use type="module" if you add the code in a script tag.
Make it yours
Components provide behavior and accessibility attributes. Styling is yours. Every example includes CSS and Tailwind versions, plus the initialization code.
<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"Show code
<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 var(--border);
margin-bottom: 1rem;
}
.tabs-trigger {
padding: 0.5rem 1rem;
background: none;
border: none;
border-bottom: 2px solid transparent;
cursor: pointer;
color: var(--muted);
margin-bottom: 0;
}
.tabs-trigger[data-state="active"] {
color: var(--text);
border-bottom-color: var(--text);
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-[var(--border)] mb-4">
<button
data-slot="tabs-trigger"
data-value="one"
class="px-4 py-2 border-b-2 border-transparent text-[var(--muted)] cursor-pointer
data-[state=active]:text-text data-[state=active]:border-text
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-[var(--muted)] cursor-pointer
data-[state=active]:text-text data-[state=active]:border-text
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>import { create } from "@data-slot/tabs";
// Initialize after the markup is in the document.
const controllers = create();
// Clean up before removing the component.
// controllers.forEach((controller) => controller.destroy());Tailwind examples use Tailwind CSS v4. Examples with animation utilities also use tw-animate-css. See the styling guide for setup.
Explore the components
Pick a component in the sidebar. Every page follows the same structure: Anatomy → Examples → API reference.