Styling
stimulus-zag is headless: it never sets a color, size, or spacing. Instead it reflects component state onto the DOM as data-* attributes, which you target with Tailwind variants (or plain CSS attribute selectors).
State attributes
Zag writes attributes like these as the machine transitions:
| Attribute | Meaning |
|---|---|
data-state="open" / "closed" | Disclosure/overlay open state |
data-highlighted | Item under keyboard/pointer focus |
data-selected | Selected tab / option |
data-disabled | Disabled part |
data-focus | Part has focus |
data-checked | Checkbox/switch/radio checked |
Style them with Tailwind's arbitrary variants:
<button
data-part="trigger"
class="data-[state=open]:bg-slate-100 data-[state=open]:text-indigo-600"
>
Menu
</button>Hide collapsed parts before the controller connects
State attributes only exist once the controller has connected. Between first paint and that moment, a part styled purely with data-[state=closed]:hidden has no data-state yet — so it renders open. For a menu that's a brief flash; for a dialog whose backdrop and positioner are fixed inset-0, it's a modal nobody can dismiss and a page nobody can click.
So author the collapsed state with the plain hidden attribute instead. Zag already drives hidden on every part it can show or hide — content, backdrop, indicators — so it clears the attribute on open and restores it on close:
<div data-part="content" hidden class="rounded-lg border bg-white p-4 shadow-lg"></div>Parts that start open (the selected tab panel, a tree branch listed in expanded-value) should be authored without it.
One caveat: hidden gets its display: none from the browser's own stylesheet, so any author rule that sets display outranks it — a part classed flex or grid stays visible while hidden. Make the attribute authoritative once, in your CSS:
[hidden] {
display: none !important;
}Then use flex/grid freely on those parts; they only apply once Zag removes hidden.
Stack popups with z-* on the content, not the positioner
Every floating component — menu, select, combobox, popover, tooltip, hover card, and the three pickers — positions itself with Zag's popper, which writes an inline z-index: var(--z-index) onto the positioner. Inline wins, so a z-50 class on the positioner never applies; worse, --z-index resolves to auto unless something defines it, and the popup then paints in document order — underneath any control further down the page.
Zag fills that variable in for you by copying the content's computed z-index up to the positioner. So put the class on the content:
<div data-part="positioner">
<div data-part="content" hidden class="z-50 rounded-lg border bg-white shadow-lg">…</div>
</div>Two components sit outside that rule:
- Dialog — Zag sets no
z-indexon its backdrop or positioner, so style those directly (class="fixed inset-0 z-50 …"). - Tour — its positioner derives
--z-indexfromcalc(var(--tour-layer) + var(--tour-z-index)), so define the base layer:class="[--tour-z-index:50]".
Positioned parts
Some parts (a color picker's gradient area, slider tracks) receive an inline position from Zag. Inline styles beat your utility classes, so fill those parts with h-full w-full rather than absolute inset-0 — the latter would be defeated by the inline position and collapse to zero size. Each component's page calls out anything like this.
Bring your own Tailwind
The demos on this site use Tailwind utilities, but the library has no opinion — plain CSS, another utility framework, or your design system all work. Nothing is bundled; you style the data-part elements directly.