@data-slot/tabs
Headless tabs component for vanilla JavaScript. Accessible, unstyled, tiny.
Installation
npm install @data-slot/tabs
Quick Start
<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>
<button data-slot="tabs-trigger" data-value="three">Tab Three</button>
<div data-slot="tabs-indicator"></div>
</div>
<div data-slot="tabs-content" data-value="one">Content One</div>
<div data-slot="tabs-content" data-value="two">Content Two</div>
<div data-slot="tabs-content" data-value="three">Content Three</div>
</div>
<script type="module">
import { create } from "@data-slot/tabs";
const controllers = create();
</script>
API
Initialization
create(scope?)
Auto-discover and bind all tabs instances in a scope (defaults to document).
import { create } from "@data-slot/tabs";
const controllers = create(); // Returns TabsController[]
createTabs(root, options?)
Create a controller for a specific element.
import { createTabs } from "@data-slot/tabs";
const tabs = createTabs(element, {
defaultValue: "one",
orientation: "horizontal",
onValueChange: (value) => console.log(value),
});
Slots
Runtime Slots
tabs- Root element that manages the selected tab and activation mode.tabs-list- Required tab-list container that receives tablist semantics and orientation.tabs-trigger- Tab button identified bydata-value; receives selected state and keyboard navigation. At least one trigger is required.tabs-content- Panel matched to its trigger bydata-value; receives tabpanel semantics and is shown when selected.tabs-indicator- Optional animated highlight whose position and size follow the selected trigger.
Markup
<div data-slot="tabs" data-default-value="initial-tab">
<div data-slot="tabs-list">
<button data-slot="tabs-trigger" data-value="unique-id">Label</button>
<!-- Optional animated indicator -->
<div data-slot="tabs-indicator"></div>
</div>
<div data-slot="tabs-content" data-value="unique-id">Panel content</div>
</div>
Data Attributes
Options can also be set via data attributes on the root element. JS options take precedence.
| Attribute | Type | Default | Description |
|---|---|---|---|
data-default-value | string | first enabled tab | Initial selected tab |
data-orientation | string | "horizontal" | Tab orientation: horizontal, vertical |
data-activation-mode | string | "auto" | Activation mode: auto, manual |
<!-- Vertical tabs with manual activation -->
<div data-slot="tabs" data-orientation="vertical" data-activation-mode="manual">
...
</div>
Options
| Option | Type | Default | Description |
|---|---|---|---|
defaultValue | string | First enabled trigger's value | Initial selected tab |
orientation | "horizontal" | "vertical" | "horizontal" | Tab orientation for keyboard nav |
activationMode | "auto" | "manual" | "auto" | How tabs are activated with keyboard |
onValueChange | (value: string) => void | undefined | Callback when selected tab changes |
Controller
| Method/Property | Description |
|---|---|
select(value) | Select a tab by value |
value | Currently selected value (readonly string) |
updateIndicator() | Recalculate indicator position after layout changes |
destroy() | Cleanup all event listeners |
Events
Outbound Events
Listen for changes via custom events:
element.addEventListener("tabs:change", (e) => {
console.log("Selected tab:", e.detail.value);
});
Inbound Events
Control the tabs via events:
| Event | Detail | Description |
|---|---|---|
tabs:set | { value: string } | Select a tab programmatically |
// Select a tab
element.dispatchEvent(
new CustomEvent("tabs:set", { detail: { value: "two" } })
);
Deprecated Events
The following event is deprecated and will be removed in v1.0:
// Deprecated: tabs:select event
element.dispatchEvent(
new CustomEvent("tabs:select", { detail: { value: "two" } })
);
// Deprecated: string detail
element.dispatchEvent(
new CustomEvent("tabs:select", { detail: "two" })
);
Use tabs:set with { value: string } instead.
Styling
Basic Styling
/* Hidden panels */
[data-slot="tabs-content"][hidden] {
display: none;
}
/* Active trigger */
[data-slot="tabs-trigger"][aria-selected="true"] {
font-weight: bold;
border-bottom: 2px solid currentColor;
}
/* Or use data-state */
[data-slot="tabs-trigger"][data-state="active"] {
color: blue;
}
[data-slot="tabs-trigger"][data-state="inactive"] {
color: gray;
}
Panel Activation Direction
Panels receive data-activation-direction after tab changes (not on initial mount):
- Horizontal:
left,right - Vertical:
up,down
Use it for directional content animations:
[data-slot="tabs-content"][data-activation-direction="right"] {
animation: slide-in-from-right 200ms ease;
}
[data-slot="tabs-content"][data-activation-direction="left"] {
animation: slide-in-from-left 200ms ease;
}
Animated Indicator
The indicator receives CSS variables for positioning:
[data-slot="tabs-indicator"] {
position: absolute;
left: var(--active-tab-left);
width: var(--active-tab-width);
height: 2px;
background: currentColor;
transition: left 0.2s, width 0.2s;
}
/* Vertical orientation */
[data-slot="tabs-list"][aria-orientation="vertical"] [data-slot="tabs-indicator"] {
top: var(--active-tab-top);
height: var(--active-tab-height);
width: 2px;
}
CSS Variables
| Variable | Description |
|---|---|
--active-tab-left | Left offset of active trigger |
--active-tab-width | Width of active trigger |
--active-tab-top | Top offset of active trigger |
--active-tab-height | Height of active trigger |
Tailwind Example
<div data-slot="tabs">
<div data-slot="tabs-list" class="relative flex border-b">
<button
data-slot="tabs-trigger"
data-value="one"
class="px-4 py-2 aria-selected:text-blue-600"
>
Tab One
</button>
<div
data-slot="tabs-indicator"
class="absolute bottom-0 h-0.5 bg-blue-600 transition-all"
style="left: var(--active-tab-left); width: var(--active-tab-width)"
></div>
</div>
<div data-slot="tabs-content" data-value="one" class="p-4">
Content
</div>
</div>
Keyboard Navigation
The tables below describe activationMode: "auto" (the default). In "manual" mode, arrow keys, Home, and End only move focus; Enter or Space selects the focused tab. Disabled triggers are skipped.
Horizontal Orientation
| Key | Action |
|---|---|
ArrowLeft | Select previous tab |
ArrowRight | Select next tab |
Home | Select first tab |
End | Select last tab |
Vertical Orientation
| Key | Action |
|---|---|
ArrowUp | Select previous tab |
ArrowDown | Select next tab |
Home | Select first tab |
End | Select last tab |
Accessibility
The component automatically handles:
role="tablist"on listrole="tab"on triggersrole="tabpanel"on contentaria-orientationon listaria-selectedon triggersaria-controlslinking triggers to panelsaria-labelledbylinking panels to triggerstabindexmanagement (only the selected enabled tab is in the tab order)
License
MIT