Introduction
stimulus-zag is a library of headless, accessible UI components for Stimulus, powered by the framework-agnostic state machines from Zag.js. It's designed for Rails + Hotwire apps, but works anywhere Stimulus runs.
"Headless" means the library provides behavior only — keyboard navigation, focus management, ARIA wiring, open/close state — and leaves all styling to you. You author plain HTML with data-part attributes that mirror each component's anatomy, add a data-controller, and style it with your own Tailwind (or plain CSS) classes.
How it works
Each component is a Stimulus controller registered under a namespaced identifier (zag-menu, zag-dialog, …). Under the hood the controller:
- Instantiates the component's Zag state machine.
- On every state change, spreads the machine's props (attributes + event handlers) onto the elements you marked with matching
data-partnames. - Exposes options as Stimulus values (
data-zag-menu-placement-value) and emits results as namespaced events (zag-menu:select).
Because state is reflected as data-* attributes, you style each state with Tailwind variants:
<button data-part="trigger" class="data-[state=open]:bg-slate-100">Menu</button>
<button data-part="item" class="data-[highlighted]:bg-indigo-500">Edit</button>Why not just use Zag directly?
Zag ships framework-agnostic machines plus a vanilla adapter, but you still have to wire the machine lifecycle, re-spread props on each change, and manage cleanup yourself for every component. stimulus-zag does that once, in a shared base controller, and gives you a consistent Stimulus-native API across all 37 components.
Ready? Head to Getting started.