Skip to main content

Building a Framework Adapter

A framework adapter connects Zag's framework-agnostic machines to your framework's reactivity, lifecycle, and DOM bindings.

machine + user props -> useMachine() -> service -> connect(service, normalizeProps) -> element props

Don't implement the machine interpreter from scratch. Port the official adapter whose reactivity model is closest to your framework:

Reactivity modelStarting point
Hook-based renderingReact or Preact
Signals and computed valuesSolid
Refs, computed values, and watchersVue
Compiler-managed reactivitySvelte
Subscriptions without a component runtimeVanilla JavaScript

Implement the Adapter

An adapter has three main responsibilities.

1. Bind the Machine

Copy machine.ts and its supporting reactive primitives from your chosen adapter. Replace the framework-specific parts:

  • Reactive state and controlled values
  • Mutable, non-reactive refs
  • Computed values and dependency tracking
  • Mount and unmount hooks
  • Synchronous update behavior

These are the main contracts to map to your framework:

Bindable

Maps controlled and uncontrolled values to reactive state.

interface Bindable<T> { initial: T | undefined ref: unknown get(): T set(value: T | ((previous: T) => T)): void invoke(nextValue: T, previousValue: T): void hash(value: T): string }

Refs

Hold mutable values without triggering a render.

interface Refs<T> { get<K extends keyof T>(key: K): T[K] set<K extends keyof T>(key: K, value: T[K]): void }

Watch

Maps machine dependencies to your framework's effect primitive.

type Track = ( dependencies: Array<() => string | number | boolean | null | undefined>, effect: () => void, ) => void
watch({ track, prop, action }) { track([() => prop("disabled")], () => { action(["syncDisabled"]) }) }

useMachine

Runs the machine lifecycle and returns the service consumed by connect.

function useMachine<T extends MachineSchema>( machine: Machine<T>, props?: Partial<T["props"]>, ): Service<T>

Keep Zag's core transition helpers and effect ordering intact.

2. Normalize DOM Props

Zag prop getters use a canonical, React-like shape. Use createNormalizer from @zag-js/types to convert those props to your framework's format.

At minimum, test:

  • Focus, blur, input, change, and double-click events
  • className, htmlFor, defaultValue, and defaultChecked
  • Style objects and CSS custom properties
  • Case-sensitive SVG attributes such as viewBox

Compare your implementation with the official normalizers.

3. Apply Props to Elements

Frameworks with prop spreading can apply a prop getter directly:

<button {...api.getTriggerProps()}>Open</button>

If your framework doesn't support this, provide a directive, action, or helper that applies attributes, properties, styles, and event listeners. It must also remove values that disappear when the props change.

Test the Adapter

Before publishing, verify:

  • Controlled and uncontrolled values stay in sync
  • User props update reactively
  • Entry and exit effects run and clean up
  • Prop normalization works for inputs, buttons, styles, and SVG
  • Event listeners update without duplicates
  • Server rendering doesn't access the DOM before mount

Use Checkbox, Tooltip, and Combobox as integration tests. Together they cover controlled state, effects, timers, keyboard events, and collections.

Declare Zag and your framework as peer dependencies, and document the version ranges you test. Adapter internals can change with Zag's machine contract, so compare your implementation with its official starting point when upgrading.

Community Adapters

These adapters are maintained by community members. They are not covered by Zag's official support or release process, so check each project's supported Zag and framework versions before adopting it.

FrameworkAdapterStatus
Angularzag-angularCommunity maintained
Markomarko-zagWork in progress

Built an adapter for another framework? Open a documentation pull request that links to its source, package, usage example, test suite, and supported version ranges.

Edit this page on GitHub