diff --git a/examples/alpine-ts/.gitignore b/examples/alpine-ts/.gitignore
new file mode 100644
index 0000000000..a547bf36d8
--- /dev/null
+++ b/examples/alpine-ts/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/examples/alpine-ts/index.html b/examples/alpine-ts/index.html
new file mode 100644
index 0000000000..ea25053491
--- /dev/null
+++ b/examples/alpine-ts/index.html
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+ Vite + TS
+
+
+ Alpine.js + Zag
+
+
+
+
diff --git a/examples/alpine-ts/package.json b/examples/alpine-ts/package.json
new file mode 100644
index 0000000000..f4df8be5f8
--- /dev/null
+++ b/examples/alpine-ts/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "alpine-ts",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "@types/alpinejs": "^3.13.11",
+ "typescript": "~5.8.3",
+ "vite": "^7.1.7"
+ },
+ "dependencies": {
+ "@zag-js/accordion": "workspace:*",
+ "@zag-js/angle-slider": "workspace:*",
+ "@zag-js/avatar": "workspace:*",
+ "@zag-js/checkbox": "workspace:*",
+ "@zag-js/collection": "workspace:*",
+ "@zag-js/combobox": "workspace:*",
+ "@zag-js/core": "workspace:*",
+ "@zag-js/dialog": "workspace:*",
+ "@zag-js/popover": "workspace:*",
+ "@zag-js/shared": "workspace:*",
+ "@zag-js/types": "workspace:*",
+ "@zag-js/utils": "workspace:*",
+ "alpinejs": "^3.15.0"
+ }
+}
diff --git a/examples/alpine-ts/pages/accordion.html b/examples/alpine-ts/pages/accordion.html
new file mode 100644
index 0000000000..4fe0a0388b
--- /dev/null
+++ b/examples/alpine-ts/pages/accordion.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+ Vite + TS
+
+
+
+ Accordion
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/alpine-ts/pages/accordion.ts b/examples/alpine-ts/pages/accordion.ts
new file mode 100644
index 0000000000..2479f1ccda
--- /dev/null
+++ b/examples/alpine-ts/pages/accordion.ts
@@ -0,0 +1,11 @@
+import "@zag-js/shared/src/style.css"
+
+import Alpine from "alpinejs"
+import * as accordion from "@zag-js/accordion"
+import { createZagPlugin } from "../src/plugin"
+
+Alpine.plugin(createZagPlugin("accordion", accordion))
+// @ts-ignore
+window.Alpine = Alpine
+// @ts-ignore
+window.Alpine.start()
diff --git a/examples/alpine-ts/pages/angle-slider.html b/examples/alpine-ts/pages/angle-slider.html
new file mode 100644
index 0000000000..6ad4c2ae4a
--- /dev/null
+++ b/examples/alpine-ts/pages/angle-slider.html
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+ Vite + TS
+
+
+
+ Angle Slider
+
+
+
+
diff --git a/examples/alpine-ts/pages/angle-slider.ts b/examples/alpine-ts/pages/angle-slider.ts
new file mode 100644
index 0000000000..b762dc5434
--- /dev/null
+++ b/examples/alpine-ts/pages/angle-slider.ts
@@ -0,0 +1,11 @@
+import "@zag-js/shared/src/style.css"
+
+import Alpine from "alpinejs"
+import * as angleSlider from "@zag-js/angle-slider"
+import { createZagPlugin } from "../src/plugin"
+
+Alpine.plugin(createZagPlugin("angle-slider", angleSlider))
+// @ts-ignore
+window.Alpine = Alpine
+// @ts-ignore
+window.Alpine.start()
diff --git a/examples/alpine-ts/pages/avatar.html b/examples/alpine-ts/pages/avatar.html
new file mode 100644
index 0000000000..0061a39ebb
--- /dev/null
+++ b/examples/alpine-ts/pages/avatar.html
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+ Vite + TS
+
+
+
+ Avatar
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![PA]()
+
+
+
+
diff --git a/examples/alpine-ts/pages/avatar.ts b/examples/alpine-ts/pages/avatar.ts
new file mode 100644
index 0000000000..b1892a8d21
--- /dev/null
+++ b/examples/alpine-ts/pages/avatar.ts
@@ -0,0 +1,11 @@
+import "@zag-js/shared/src/style.css"
+
+import Alpine from "alpinejs"
+import * as avatar from "@zag-js/avatar"
+import { createZagPlugin } from "../src/plugin"
+
+Alpine.plugin(createZagPlugin("avatar", avatar))
+// @ts-ignore
+window.Alpine = Alpine
+// @ts-ignore
+window.Alpine.start()
diff --git a/examples/alpine-ts/pages/checkbox.html b/examples/alpine-ts/pages/checkbox.html
new file mode 100644
index 0000000000..5de46c82a7
--- /dev/null
+++ b/examples/alpine-ts/pages/checkbox.html
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+ Vite + TS
+
+
+
+ Checkbox
+
+
+
+
diff --git a/examples/alpine-ts/pages/checkbox.ts b/examples/alpine-ts/pages/checkbox.ts
new file mode 100644
index 0000000000..3c6934e6b7
--- /dev/null
+++ b/examples/alpine-ts/pages/checkbox.ts
@@ -0,0 +1,11 @@
+import "@zag-js/shared/src/style.css"
+
+import Alpine from "alpinejs"
+import * as checkbox from "@zag-js/checkbox"
+import { createZagPlugin } from "../src/plugin"
+
+Alpine.plugin(createZagPlugin("checkbox", checkbox))
+// @ts-ignore
+window.Alpine = Alpine
+// @ts-ignore
+window.Alpine.start()
diff --git a/examples/alpine-ts/pages/combobox.html b/examples/alpine-ts/pages/combobox.html
new file mode 100644
index 0000000000..e36fe6f3c9
--- /dev/null
+++ b/examples/alpine-ts/pages/combobox.html
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+ Vite + TS
+
+
+
+ Combobox
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/alpine-ts/pages/combobox.ts b/examples/alpine-ts/pages/combobox.ts
new file mode 100644
index 0000000000..3b263f4f20
--- /dev/null
+++ b/examples/alpine-ts/pages/combobox.ts
@@ -0,0 +1,11 @@
+import "@zag-js/shared/src/style.css"
+
+import Alpine from "alpinejs"
+import * as combobox from "@zag-js/combobox"
+import { createZagPlugin } from "../src/plugin"
+
+Alpine.plugin(createZagPlugin("combobox", combobox))
+// @ts-ignore
+window.Alpine = Alpine
+// @ts-ignore
+window.Alpine.start()
diff --git a/examples/alpine-ts/pages/dialog.html b/examples/alpine-ts/pages/dialog.html
new file mode 100644
index 0000000000..9637e36770
--- /dev/null
+++ b/examples/alpine-ts/pages/dialog.html
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+ Vite + TS
+
+
+
+ Popover
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Title
+
Description
+
+
+
+
+
+
+
+
+
diff --git a/examples/alpine-ts/pages/dialog.ts b/examples/alpine-ts/pages/dialog.ts
new file mode 100644
index 0000000000..c2283e3ba3
--- /dev/null
+++ b/examples/alpine-ts/pages/dialog.ts
@@ -0,0 +1,11 @@
+import "@zag-js/shared/src/style.css"
+
+import Alpine from "alpinejs"
+import * as dialog from "@zag-js/dialog"
+import { createZagPlugin } from "../src/plugin"
+
+Alpine.plugin(createZagPlugin("dialog", dialog))
+// @ts-ignore
+window.Alpine = Alpine
+// @ts-ignore
+window.Alpine.start()
diff --git a/examples/alpine-ts/pages/popover.html b/examples/alpine-ts/pages/popover.html
new file mode 100644
index 0000000000..ddf552fb9b
--- /dev/null
+++ b/examples/alpine-ts/pages/popover.html
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+ Vite + TS
+
+
+
+ Popover
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Title
+
Description
+
+
+
+
+
+
+
+
diff --git a/examples/alpine-ts/pages/popover.ts b/examples/alpine-ts/pages/popover.ts
new file mode 100644
index 0000000000..a077776956
--- /dev/null
+++ b/examples/alpine-ts/pages/popover.ts
@@ -0,0 +1,11 @@
+import "@zag-js/shared/src/style.css"
+
+import Alpine from "alpinejs"
+import * as popover from "@zag-js/popover"
+import { createZagPlugin } from "../src/plugin"
+
+Alpine.plugin(createZagPlugin("popover", popover))
+// @ts-ignore
+window.Alpine = Alpine
+// @ts-ignore
+window.Alpine.start()
diff --git a/examples/alpine-ts/public/vite.svg b/examples/alpine-ts/public/vite.svg
new file mode 100644
index 0000000000..e7b8dfb1b2
--- /dev/null
+++ b/examples/alpine-ts/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/alpine-ts/src/lib/bindable.ts b/examples/alpine-ts/src/lib/bindable.ts
new file mode 100644
index 0000000000..602e8f956e
--- /dev/null
+++ b/examples/alpine-ts/src/lib/bindable.ts
@@ -0,0 +1,53 @@
+import type { Bindable, BindableParams } from "@zag-js/core"
+import { isFunction } from "@zag-js/utils"
+import Alpine from "alpinejs"
+
+export function bindable(props: () => BindableParams): Bindable {
+ const initial = props().defaultValue ?? props().value
+ const eq = props().isEqual ?? Object.is
+
+ const v = Alpine.reactive({ value: initial as T })
+
+ const controlled = () => props().value !== undefined
+
+ return {
+ initial,
+ ref: v,
+ get() {
+ return controlled() ? (props().value as T) : v.value
+ },
+ set(val: T | ((prev: T) => T)) {
+ const prev = controlled() ? (props().value as T) : v.value
+ const next = isFunction(val) ? val(prev) : val
+
+ if (props().debug) {
+ console.log(`[bindable > ${props().debug}] setValue`, { next, prev })
+ }
+
+ if (!controlled()) v.value = next
+ if (!eq(next, prev)) {
+ props().onChange?.(next, prev)
+ }
+ },
+ invoke(nextValue: T, prevValue: T) {
+ props().onChange?.(nextValue, prevValue)
+ },
+ hash(value: T) {
+ return props().hash?.(value) ?? String(value)
+ },
+ }
+}
+
+bindable.cleanup = (_fn: VoidFunction) => {
+ // No-op in vanilla implementation
+}
+
+bindable.ref = (defaultValue: T) => {
+ let value = defaultValue
+ return {
+ get: () => value,
+ set: (next: T) => {
+ value = next
+ },
+ }
+}
diff --git a/examples/alpine-ts/src/lib/index.ts b/examples/alpine-ts/src/lib/index.ts
new file mode 100644
index 0000000000..e0d86db4f4
--- /dev/null
+++ b/examples/alpine-ts/src/lib/index.ts
@@ -0,0 +1,3 @@
+export { mergeProps } from "@zag-js/core"
+export * from "./normalize-props"
+export * from "./machine"
diff --git a/examples/alpine-ts/src/lib/machine.ts b/examples/alpine-ts/src/lib/machine.ts
new file mode 100644
index 0000000000..b50bc97c0f
--- /dev/null
+++ b/examples/alpine-ts/src/lib/machine.ts
@@ -0,0 +1,312 @@
+import type {
+ ActionsOrFn,
+ Bindable,
+ BindableContext,
+ BindableRefs,
+ ChooseFn,
+ ComputedFn,
+ EffectsOrFn,
+ GuardFn,
+ Machine,
+ MachineSchema,
+ Params,
+ PropFn,
+ Scope,
+ Service,
+} from "@zag-js/core"
+import { createScope, INIT_STATE, MachineStatus } from "@zag-js/core"
+import { compact, isFunction, isString, toArray, warn } from "@zag-js/utils"
+import Alpine from "alpinejs"
+import { bindable } from "./bindable"
+import { createRefs } from "./refs"
+import { track } from "./track"
+
+export class AlpineMachine implements Service {
+ scope: Scope = null as any
+ ctx: BindableContext
+ prop: PropFn
+ private _state: Bindable
+ refs: BindableRefs
+ computed: ComputedFn
+
+ private _event: any = { type: "" }
+ private previousEvent: any = null
+
+ private effects = new Map()
+ private transition: any = null
+
+ private status = MachineStatus.NotStarted
+
+ private getEvent() {
+ return {
+ ...this._event,
+ current: () => this._event,
+ previous: () => this.previousEvent,
+ }
+ }
+
+ get event(): any {
+ return this.getEvent()
+ }
+
+ getStatus(): MachineStatus {
+ return this.status
+ }
+
+ get context(): BindableContext {
+ return this.ctx
+ }
+
+ get state(): Bindable & {
+ matches: (...values: T["state"][]) => boolean
+ hasTag: (tag: T["tag"]) => boolean
+ } {
+ return this.getState()
+ }
+
+ private getState(): Bindable & {
+ matches: (...values: T["state"][]) => boolean
+ hasTag: (tag: T["tag"]) => boolean
+ } {
+ return {
+ ...this._state,
+ matches: (...values: T["state"][]) => values.includes(this._state.get()),
+ hasTag: (tag: T["tag"]) => !!this.machine.states[this._state.get()]?.tags?.includes(tag),
+ }
+ }
+
+ private debug(...args: any[]) {
+ if (this.machine.debug) console.log(...args)
+ }
+
+ constructor(
+ private machine: Machine,
+ evaluateProps: (callback: (userProps: Partial) => void) => void,
+ ) {
+ // create scope
+ evaluateProps((userProps) => {
+ const { id, ids, getRootNode } = userProps as any
+ this.scope = createScope({ id, ids, getRootNode })
+ })
+
+ // create prop
+ this.prop = (key) => {
+ let value
+ evaluateProps((userProps) => {
+ const props =
+ machine.props?.({
+ props: compact(userProps),
+ scope: this.scope,
+ }) ?? userProps
+ value = props[key]
+ })
+ return value as T["props"][typeof key]
+ }
+
+ // create context
+ const _context = machine.context?.({
+ prop: this.prop,
+ bindable,
+ scope: this.scope,
+ flush() {},
+ getContext: () => this.ctx,
+ getComputed: () => this.computed,
+ getRefs: () => this.refs,
+ getEvent: this.getEvent.bind(this),
+ })
+
+ // context function
+ this.ctx = {
+ get(key) {
+ return _context?.[key].get() as T["context"][typeof key]
+ },
+ set(key, value) {
+ _context?.[key].set(value)
+ },
+ initial(key) {
+ return _context?.[key].initial as T["context"][typeof key]
+ },
+ hash(key) {
+ const current = _context?.[key].get() as T["context"][typeof key]
+ return _context?.[key].hash(current) as string
+ },
+ }
+
+ // create computed
+ this.computed = (key) =>
+ machine.computed?.[key]({
+ context: this.ctx,
+ event: this.getEvent(),
+ prop: this.prop,
+ refs: this.refs,
+ scope: this.scope,
+ computed: this.computed,
+ }) ?? ({} as any)
+
+ // create refs
+ this.refs = createRefs(machine.refs?.({ prop: this.prop, context: this.ctx }))
+
+ // create state
+ this._state = bindable(() => ({
+ defaultValue: machine.initialState({ prop: this.prop }),
+ onChange: (nextState, prevState) => {
+ // compute effects: exit -> transition -> enter
+
+ // exit effects
+ if (prevState) {
+ const exitEffects = this.effects.get(prevState)
+ exitEffects?.()
+ this.effects.delete(prevState)
+ }
+
+ // exit actions
+ if (prevState) {
+ this.action(machine.states[prevState]?.exit)
+ }
+
+ // transition actions
+ this.action(this.transition?.actions)
+
+ // enter effect
+ const cleanup = this.effect(machine.states[nextState]?.effects)
+ if (cleanup) this.effects.set(nextState as string, cleanup)
+
+ // root entry actions
+ if (prevState === INIT_STATE) {
+ this.action(machine.entry)
+ const cleanup = this.effect(machine.effects)
+ if (cleanup) this.effects.set(INIT_STATE, cleanup)
+ }
+
+ // enter actions
+ this.action(machine.states[nextState]?.entry)
+ },
+ }))
+ }
+
+ send = (event: any) => {
+ if (this.status !== MachineStatus.Started) return
+
+ this.previousEvent = this._event
+ this._event = event
+
+ this.debug("send", event)
+
+ const currentState = this.state.get()
+
+ const transitions =
+ // @ts-ignore transition
+ this.machine.states[currentState].on?.[event.type] ??
+ // @ts-ignore transition
+ this.machine.on?.[event.type]
+
+ const transition = this.choose(transitions)
+ if (!transition) return
+
+ // save current transition
+ this.transition = transition
+ const target = transition.target ?? currentState
+
+ this.debug("transition", transition)
+
+ const changed = target !== currentState
+ if (changed) {
+ // state change is high priority
+ this.state.set(target)
+ } else if (transition.reenter && !changed) {
+ // reenter will re-invoke the current state
+ this.state.invoke(currentState, currentState)
+ } else {
+ // call transition actions
+ this.action(transition.actions)
+ }
+ }
+
+ private action = (keys: ActionsOrFn | undefined) => {
+ const strs = isFunction(keys) ? keys(this.getParams()) : keys
+ if (!strs) return
+ const fns = strs.map((s) => {
+ const fn = this.machine.implementations?.actions?.[s]
+ if (!fn) {
+ warn(`[zag-js] No implementation found for action "${JSON.stringify(s)}"`)
+ }
+ return fn
+ })
+ for (const fn of fns) {
+ fn?.(this.getParams())
+ }
+ }
+
+ private guard = (str: T["guard"] | GuardFn) => {
+ if (isFunction(str)) return str(this.getParams())
+ return this.machine.implementations?.guards?.[str](this.getParams())
+ }
+
+ private effect = (keys: EffectsOrFn | undefined) => {
+ const strs = isFunction(keys) ? keys(this.getParams()) : keys
+ if (!strs) return
+ const fns = strs.map((s) => {
+ const fn = this.machine.implementations?.effects?.[s]
+ if (!fn) {
+ warn(`[zag-js] No implementation found for effect "${JSON.stringify(s)}"`)
+ }
+ return fn
+ })
+ const cleanups: VoidFunction[] = []
+ for (const fn of fns) {
+ const cleanup = fn?.(this.getParams())
+ if (cleanup) cleanups.push(cleanup)
+ }
+ return () => cleanups.forEach((fn) => fn?.())
+ }
+
+ private choose: ChooseFn = (transitions) => {
+ return toArray(transitions).find((t) => {
+ let result = !t.guard
+ if (isString(t.guard)) result = !!this.guard(t.guard)
+ else if (isFunction(t.guard)) result = t.guard(this.getParams())
+ return result
+ })
+ }
+
+ init() {
+ this.status = MachineStatus.Started
+ this.debug("initializing...")
+ this.state.invoke(this.state.initial, INIT_STATE)
+ this.machine.watch?.(this.getParams())
+ }
+
+ destroy() {
+ this.effects.forEach((fn) => fn?.())
+ this.effects.clear()
+ this.transition = null
+ this.action(this.machine.exit)
+
+ this.status = MachineStatus.Stopped
+ this.debug("unmounting...")
+ }
+
+ getParams(): Params {
+ return {
+ state: this.getState(),
+ context: this.ctx,
+ event: this.getEvent(),
+ prop: this.prop,
+ send: this.send,
+ action: this.action,
+ guard: this.guard,
+ track,
+ refs: this.refs,
+ computed: this.computed,
+ flush,
+ scope: this.scope,
+ choose: this.choose,
+ }
+ }
+}
+
+const flush = (fn: VoidFunction) => {
+ Alpine.nextTick(() => {
+ fn()
+ })
+}
diff --git a/examples/alpine-ts/src/lib/normalize-props.ts b/examples/alpine-ts/src/lib/normalize-props.ts
new file mode 100644
index 0000000000..df1bcb593f
--- /dev/null
+++ b/examples/alpine-ts/src/lib/normalize-props.ts
@@ -0,0 +1,26 @@
+import { createNormalizer } from "@zag-js/types"
+
+const propMap: Record = {
+ onFocus: "onFocusin",
+ onBlur: "onFocusout",
+ onChange: "onInput",
+ onDoubleClick: "onDblclick",
+ htmlFor: "for",
+ className: "class",
+ defaultValue: "value",
+ defaultChecked: "checked",
+}
+
+export const normalizeProps = createNormalizer((props) => {
+ return Object.entries(props).reduce>((acc, [key, value]) => {
+ if (key in propMap) {
+ key = propMap[key]
+ }
+ if (key.startsWith("on")) {
+ acc["@" + key.substring(2).toLowerCase()] = value
+ return acc
+ }
+ acc[":" + key.toLowerCase()] = () => value
+ return acc
+ }, {})
+})
diff --git a/examples/alpine-ts/src/lib/refs.ts b/examples/alpine-ts/src/lib/refs.ts
new file mode 100644
index 0000000000..81057207f3
--- /dev/null
+++ b/examples/alpine-ts/src/lib/refs.ts
@@ -0,0 +1,11 @@
+export function createRefs(refs: T) {
+ const ref = { current: refs }
+ return {
+ get(key: K): T[K] {
+ return ref.current[key]
+ },
+ set(key: K, value: T[K]) {
+ ref.current[key] = value
+ },
+ }
+}
diff --git a/examples/alpine-ts/src/lib/track.ts b/examples/alpine-ts/src/lib/track.ts
new file mode 100644
index 0000000000..b3768ecec6
--- /dev/null
+++ b/examples/alpine-ts/src/lib/track.ts
@@ -0,0 +1,30 @@
+import Alpine from "alpinejs"
+import { isEqual, isFunction } from "@zag-js/utils"
+
+function access(value: any) {
+ if (isFunction(value)) return value()
+ return value
+}
+
+export const track = (deps: any[], effect: VoidFunction) => {
+ let prevDeps: any[] = []
+ let isFirstRun = true
+ Alpine.effect(() => {
+ if (isFirstRun) {
+ prevDeps = deps.map((d) => access(d))
+ isFirstRun = false
+ return
+ }
+ let changed = false
+ for (let i = 0; i < deps.length; i++) {
+ if (!isEqual(prevDeps[i], access(deps[i]))) {
+ changed = true
+ break
+ }
+ }
+ if (changed) {
+ prevDeps = deps.map((d) => access(d))
+ effect()
+ }
+ })
+}
diff --git a/examples/alpine-ts/src/plugin.ts b/examples/alpine-ts/src/plugin.ts
new file mode 100644
index 0000000000..0daa8b5e08
--- /dev/null
+++ b/examples/alpine-ts/src/plugin.ts
@@ -0,0 +1,89 @@
+import type { Alpine, ElementWithXAttributes } from "alpinejs"
+import type { Machine, MachineSchema, Service } from "@zag-js/core"
+import type { NormalizeProps, PropTypes } from "@zag-js/types"
+import type { ListCollection, CollectionItem, CollectionOptions } from "@zag-js/collection"
+import { AlpineMachine, normalizeProps } from "./lib"
+
+export function createZagPlugin(
+ name: string,
+ component: {
+ machine: Machine
+ connect: (service: Service, normalizeProps: NormalizeProps) => any
+ collection?: (options: CollectionOptions) => ListCollection
+ },
+) {
+ const underScore = name.replaceAll("-", "_")
+ const api = `_${underScore}_api`
+ const bindings = `_${underScore}_bindings`
+
+ return function (Alpine: Alpine) {
+ Alpine.directive(name, (el, { expression, value }, { evaluateLater, evaluate }) => {
+ if (!value) {
+ const service = new AlpineMachine(component.machine, evaluateLater(expression))
+ Alpine.bind(el, {
+ "x-data"() {
+ return {
+ [api]: component.connect(service, normalizeProps),
+ [bindings]: [] as {
+ el: ElementWithXAttributes
+ getProps: string
+ props: any
+ cleanup: () => void
+ }[],
+ init() {
+ queueMicrotask(() => {
+ Alpine.effect(() => {
+ this[api] = component.connect(service, normalizeProps)
+
+ for (const binding of this[bindings]) {
+ // 'spread props' by cleaning up and re-binding
+ binding.cleanup()
+ binding.cleanup = Alpine.bind(binding.el, this[api][binding.getProps](binding.props))
+ }
+ })
+ })
+ service.init()
+ },
+ destroy() {
+ for (const binding of this[bindings]) {
+ binding.cleanup()
+ }
+ service.destroy()
+ },
+ }
+ },
+ })
+ } else if (value === "collection") {
+ Alpine.bind(el, {
+ "x-data"() {
+ return {
+ get collection() {
+ return component.collection?.(evaluate(expression) as any)
+ },
+ }
+ },
+ })
+ } else {
+ ;(Alpine.$data(el) as any)[bindings].push({
+ el,
+ getProps: `get${value
+ .split("-")
+ .map((v) => v.at(0)?.toUpperCase() + v.substring(1).toLowerCase())
+ .join("")}Props`,
+ get props() {
+ return expression ? evaluate(expression) : {}
+ },
+ cleanup: () => {},
+ })
+ }
+ }).before("bind")
+
+ Alpine.magic(
+ name
+ .split("-")
+ .map((str, i) => (i === 0 ? str : str.at(0)?.toUpperCase() + str.substring(1).toLowerCase()))
+ .join(""),
+ (el) => (Alpine.$data(el) as any)[api],
+ )
+ }
+}
diff --git a/examples/alpine-ts/src/typescript.svg b/examples/alpine-ts/src/typescript.svg
new file mode 100644
index 0000000000..d91c910cc3
--- /dev/null
+++ b/examples/alpine-ts/src/typescript.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/alpine-ts/tsconfig.json b/examples/alpine-ts/tsconfig.json
new file mode 100644
index 0000000000..6feba115c2
--- /dev/null
+++ b/examples/alpine-ts/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "types": ["vite/client"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["src"]
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 58b5341c31..c50165e89d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -132,6 +132,58 @@ importers:
specifier: 3.2.4
version: 3.2.4(@types/debug@4.1.12)(@types/node@24.7.0)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.25.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)
+ examples/alpine-ts:
+ dependencies:
+ '@zag-js/accordion':
+ specifier: workspace:*
+ version: link:../../packages/machines/accordion
+ '@zag-js/angle-slider':
+ specifier: workspace:*
+ version: link:../../packages/machines/angle-slider
+ '@zag-js/avatar':
+ specifier: workspace:*
+ version: link:../../packages/machines/avatar
+ '@zag-js/checkbox':
+ specifier: workspace:*
+ version: link:../../packages/machines/checkbox
+ '@zag-js/collection':
+ specifier: workspace:*
+ version: link:../../packages/utilities/collection
+ '@zag-js/combobox':
+ specifier: workspace:*
+ version: link:../../packages/machines/combobox
+ '@zag-js/core':
+ specifier: workspace:*
+ version: link:../../packages/core
+ '@zag-js/dialog':
+ specifier: workspace:*
+ version: link:../../packages/machines/dialog
+ '@zag-js/popover':
+ specifier: workspace:*
+ version: link:../../packages/machines/popover
+ '@zag-js/shared':
+ specifier: workspace:*
+ version: link:../../shared
+ '@zag-js/types':
+ specifier: workspace:*
+ version: link:../../packages/types
+ '@zag-js/utils':
+ specifier: workspace:*
+ version: link:../../packages/utilities/core
+ alpinejs:
+ specifier: ^3.15.0
+ version: 3.15.0
+ devDependencies:
+ '@types/alpinejs':
+ specifier: ^3.13.11
+ version: 3.13.11
+ typescript:
+ specifier: ~5.8.3
+ version: 5.8.3
+ vite:
+ specifier: ^7.1.7
+ version: 7.1.7(@types/node@24.5.2)(jiti@2.6.0)(lightningcss@1.25.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)
+
examples/next-ts:
dependencies:
'@internationalized/date':
@@ -5941,6 +5993,9 @@ packages:
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
+ '@types/alpinejs@3.13.11':
+ resolution: {integrity: sha512-3KhGkDixCPiLdL3Z/ok1GxHwLxEWqQOKJccgaQL01wc0EVM2tCTaqlC3NIedmxAXkVzt/V6VTM8qPgnOHKJ1MA==}
+
'@types/argparse@1.0.38':
resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==}
@@ -6479,6 +6534,9 @@ packages:
peerDependencies:
vue: 3.5.22
+ '@vue/shared@3.1.5':
+ resolution: {integrity: sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==}
+
'@vue/shared@3.4.19':
resolution: {integrity: sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw==}
@@ -6564,6 +6622,9 @@ packages:
engines: {node: '>=4'}
hasBin: true
+ alpinejs@3.15.0:
+ resolution: {integrity: sha512-lpokA5okCF1BKh10LG8YjqhfpxyHBk4gE7boIgVHltJzYoM7O9nK3M7VlntLEJGsVmu7U/RzUWajmHREGT38Eg==}
+
ansi-align@3.0.1:
resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
@@ -14649,6 +14710,8 @@ snapshots:
tslib: 2.8.1
optional: true
+ '@types/alpinejs@3.13.11': {}
+
'@types/argparse@1.0.38': {}
'@types/aria-query@5.0.4': {}
@@ -15430,6 +15493,8 @@ snapshots:
'@vue/shared': 3.5.22
vue: 3.5.22(typescript@5.9.3)
+ '@vue/shared@3.1.5': {}
+
'@vue/shared@3.4.19': {}
'@vue/shared@3.5.22': {}
@@ -15522,6 +15587,10 @@ snapshots:
transitivePeerDependencies:
- encoding
+ alpinejs@3.15.0:
+ dependencies:
+ '@vue/reactivity': 3.1.5
+
ansi-align@3.0.1:
dependencies:
string-width: 4.2.3