Styling
Your HTML. Your styles. The same accessible behavior.
Components are unstyled by default. Use regular CSS, Tailwind utilities, or your existing design system.
The docs examples use square corners for triggers, panels, menus, and items in both CSS and Tailwind. Round shapes are reserved for radio buttons, switches, slider controls, avatars, status icons, and drawer handles. You can adapt these styles to your own design system.
CSS
Add classes to your markup, then style the state attributes set by the component. Keep selectors scoped to a component or example to avoid affecting other parts of your application.
.tabs-trigger {
padding: 0.5rem 1rem;
border-bottom: 2px solid transparent;
}
.tabs-trigger[data-state="active"] {
border-bottom-color: currentColor;
font-weight: 500;
}
.tabs-content[hidden] {
display: none;
}
Tailwind
The examples use Tailwind CSS v4. Add Tailwind to your build, then import the stylesheet in your application’s CSS entry.
@import "tailwindcss";
Use data-* and aria-* variants to respond to component state.
<button
data-slot="tabs-trigger"
data-value="one"
class="border-b-2 border-transparent px-4 py-2 data-[state=active]:border-current data-[state=active]:font-medium"
>
Overview
</button>
The examples share a semantic color palette and follow the docs’ light or dark theme. Define the variables used in the copied markup and CSS, then expose them as Tailwind tokens:
:root {
color-scheme: light;
--bg: #faf9f7;
--text: #1a1a1a;
--muted: #6c6a66;
--border: #d6d3cd;
--code-bg: #f0eeeb;
--surface: #fff;
--accent: #0066cc;
--danger: #c62828;
--danger-bg: #fff1f0;
}
:root[data-theme="dark"] {
color-scheme: dark;
--bg: #191918;
--text: #eae7e1;
--muted: #a6a39c;
--border: #3b3a36;
--code-bg: #242423;
--surface: #242423;
--accent: #75baff;
--danger: #ff9494;
--danger-bg: #3b2424;
}
@theme inline {
--color-bg: var(--bg);
--color-text: var(--text);
--color-muted: var(--muted);
--color-border: var(--border);
--color-accent: var(--accent);
--color-code-bg: var(--code-bg);
}
CSS examples use the same variables without the @theme block. Set data-theme="dark" on the document’s <html> element, or adapt the selector to your theme switcher. Keeping the variables on the document root also themes popups that are portaled to document.body.
Animation utilities
Examples using animate-in, fade-in, or accordion animation utilities require tw-animate-css:
npm install tw-animate-css
@import "tailwindcss";
@import "tw-animate-css";
CSS variables
Components expose measurements as CSS variables for content sizing, indicator positioning, and animation. Use them with var() in CSS or Tailwind utilities such as h-(--accordion-panel-height) and w-(--active-tab-width).
For navigation menus, style the popup shell with --popup-width and --popup-height. These measured pixel values let CSS animate size changes when switching panels. Use --positioner-width and --positioner-height to size the positioner, --available-width and --available-height to constrain the popup to the available space, and --transform-origin to anchor animations to the trigger.
[data-slot="navigation-menu-popup"] {
width: var(--popup-width);
height: var(--popup-height);
max-width: var(--available-width);
max-height: var(--available-height);
transform-origin: var(--transform-origin);
transition: width 200ms, height 200ms;
}
The navigation menu’s --viewport-width and --viewport-height variables are deprecated aliases for the active panel dimensions. Use the popup variables for new styles. Each component’s API reference lists its available variables and the elements that expose them.
State and accessibility
The API reference on each component page documents the attributes and CSS variables it exposes. These vary by component: tabs use data-state="active", toggles use data-state="on", and switches expose data-checked and data-unchecked.
Preserve visible keyboard focus and the component’s hidden states. Use real buttons for actions and give icon-only controls an accessible name with aria-label.
button:focus-visible {
outline: 2px solid currentColor;
outline-offset: 4px;
}
@media (prefers-reduced-motion: reduce) {
.accordion-content {
transition: none;
}
}
Try both styles
Use the styling buttons on any example. Your preference is remembered for the next page. Open Show code to inspect the matching HTML and its JavaScript setup.
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.Show code
<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">
<span>What makes data-slot different?</span>
<span data-slot="accordion-trigger-icon" class="accordion-trigger-icon">+</span>
</button>
<div data-slot="accordion-content" class="accordion-content">
<div data-slot="accordion-content-inner" class="accordion-content-inner">
Unlike React-based headless libraries, data-slot works with vanilla HTML.
</div>
</div>
</div>
<div data-slot="accordion-item" data-value="accessibility" class="accordion-item">
<button data-slot="accordion-trigger" class="accordion-trigger">
<span>Is it accessible?</span>
<span data-slot="accordion-trigger-icon" class="accordion-trigger-icon">+</span>
</button>
<div data-slot="accordion-content" class="accordion-content">
<div data-slot="accordion-content-inner" class="accordion-content-inner">
Yes. All components implement WAI-ARIA patterns.
</div>
</div>
</div>
</div>
<style>
.accordion-item { border-bottom: 1px dashed var(--border); }
.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;
align-items: center;
gap: 1rem;
}
.accordion-trigger-icon {
margin-left: auto;
color: var(--muted);
transition: transform 0.25s ease;
}
.accordion-item[data-state="open"] .accordion-trigger-icon {
transform: rotate(45deg);
}
.accordion-content {
overflow: hidden;
height: var(--accordion-panel-height);
transition: height 0.3s ease-out;
}
.accordion-content[data-starting-style] {
height: 0;
}
.accordion-content-inner { padding: 0 0 1rem; color: var(--muted); }
</style><div data-slot="accordion" data-default-value="features">
<div
data-slot="accordion-item"
data-value="features"
class="group border-b border-dashed border-[var(--border)] last:border-b-0"
>
<button
data-slot="accordion-trigger"
class="flex w-full items-center gap-4 bg-transparent py-3 text-left"
>
<span>What makes data-slot different?</span>
<span
data-slot="accordion-trigger-icon"
class="ml-auto shrink-0 text-[var(--muted)] transition-transform group-data-[state=open]:rotate-45"
>
+
</span>
</button>
<div
data-slot="accordion-content"
class="overflow-hidden h-(--accordion-panel-height) transition-all duration-300 ease-out data-starting-style:h-0"
>
<div
data-slot="accordion-content-inner"
class="pb-4 pt-0 text-[var(--muted)]"
>
Unlike React-based headless libraries, data-slot works with vanilla HTML.
</div>
</div>
</div>
<div
data-slot="accordion-item"
data-value="accessibility"
class="group border-b border-dashed border-[var(--border)] last:border-b-0"
>
<button
data-slot="accordion-trigger"
class="flex w-full items-center gap-4 bg-transparent py-3 text-left"
>
<span>Is it accessible?</span>
<span
data-slot="accordion-trigger-icon"
class="ml-auto shrink-0 text-[var(--muted)] transition-transform group-data-[state=open]:rotate-45"
>
+
</span>
</button>
<div
data-slot="accordion-content"
class="overflow-hidden h-(--accordion-panel-height) transition-all duration-300 ease-out data-starting-style:h-0"
>
<div
data-slot="accordion-content-inner"
class="pb-4 pt-0 text-[var(--muted)]"
>
Yes. All components implement WAI-ARIA patterns.
</div>
</div>
</div>
</div>import { create } from "@data-slot/accordion";
// Initialize after the markup is in the document.
const controllers = create();
// Clean up before removing the component.
// controllers.forEach((controller) => controller.destroy());