Getting started
Install
npm install stimulus-zag@hotwired/stimulus is a peer dependency (v3+); the relevant @zag-js/* packages come along as dependencies.
Register the controllers
Register everything at once:
import { Application } from "@hotwired/stimulus"
import { registerStimulusZag } from "stimulus-zag"
const application = Application.start()
registerStimulusZag(application)Or, when you bundle your JavaScript, register only what you use so the rest tree-shakes away. The register helper reads each controller's identifier:
import { Application } from "@hotwired/stimulus"
import { register, MenuController } from "stimulus-zag"
const application = Application.start()
register(application, MenuController)Each controller is also available on its own subpath, if you prefer explicit per-component imports (stimulus-zag/<component>):
import { MenuController } from "stimulus-zag/menu"
import { DialogController } from "stimulus-zag/dialog"Both forms bundle the same thanks to tree-shaking; the subpath is just more explicit and works in environments that don't tree-shake.
Rails
Using importmap-rails or esbuild / jsbundling? See the Rails guide — the companion gem removes the need to pin every Zag package for importmap apps, and there's a bundle-size breakdown for selective registration.
Add a component
Author the markup with data-part attributes that match the component's anatomy, add the data-controller, and style it however you like:
<div data-controller="zag-menu" class="relative inline-block text-left">
<button
data-part="trigger"
class="border-input bg-popover text-foreground hover:bg-accent focus-visible:ring-ring data-[state=open]:bg-accent inline-flex items-center gap-2 rounded-lg border px-4 py-2 text-sm font-medium shadow-sm transition focus:outline-none focus-visible:ring-2"
>
Actions
<span
data-part="indicator"
class="text-muted-foreground transition-transform data-[state=open]:rotate-180"
aria-hidden="true"
>▾</span
>
</button>
<div data-part="positioner">
<div
data-part="content"
hidden
class="border-border bg-popover z-50 min-w-44 rounded-lg border p-1 shadow-lg focus:outline-none"
>
<button
data-part="item"
data-value="edit"
class="text-foreground data-[highlighted]:bg-primary data-[highlighted]:text-primary-foreground flex w-full cursor-pointer items-center rounded-md px-3 py-2 text-sm"
>
Edit
</button>
<button
data-part="item"
data-value="duplicate"
class="text-foreground data-[highlighted]:bg-primary data-[highlighted]:text-primary-foreground flex w-full cursor-pointer items-center rounded-md px-3 py-2 text-sm"
>
Duplicate
</button>
<hr data-part="separator" class="border-border my-1" />
<button
data-part="item"
data-value="delete"
data-disabled
class="text-destructive data-[highlighted]:bg-destructive flex w-full items-center rounded-md px-3 py-2 text-sm data-[disabled]:cursor-not-allowed data-[highlighted]:text-white data-[disabled]:opacity-40"
>
Delete
</button>
</div>
</div>
</div>That's it — no build-time templates, no per-component setup. The controller connects on page load (and after any Turbo navigation).
Next steps
- Learn how state maps to styling hooks.
- Browse the components.