Skip to main content
After years of refinement, Zag v1 moves from an external store to the native reactive primitives of each supported framework. This delivers significant performance and bundle-size improvements while simplifying the API for controlled and uncontrolled state.

What changed

useMachine signature

useMachine now returns a service object instead of a [state, send] tuple. The machine definition is passed directly as an object — no longer as a function call.
const [state, send] = useMachine(avatar.machine({ id: useId() }))
Use “Find and Replace” across your codebase to migrate useMachine call sites quickly. The pattern is consistent across all components.

TypeScript caveat for generic components

Because machine is now an object rather than a function, TypeScript inference is limited for generic components such as combobox and select. Use the exported Machine type to cast:
import * as combobox from "@zag-js/combobox"

useMachine(combobox.machine as combobox.Machine<Item>)

Controlled vs uncontrolled value

Previously, you passed the initial value to the machine constructor and a controlled value inside a separate context option — a pattern that was error-prone and confusing. Now both options live at the same level with explicit names:
const [state, send] = useMachine(numberInput.machine({ value: "10" }), {
  context: {
    value: "10", // controlled
  },
})
This change applies to all components that have a value prop.

Controlled vs uncontrolled open state

The same pattern applies to disclosure components (Dialog, Popover, Tooltip, etc.). The open.controlled workaround is removed:
const [state, send] = useMachine(dialog.machine({ open: true }), {
  context: {
    open: true,
    "open.controlled": true,
  },
})

Type renames

The Context type exported by each component package is renamed to Props:
import * as accordion from "@zag-js/accordion"

interface MyProps extends accordion.Context {}

Toast

The toast component now requires you to create an explicit store (manager) and pass it to the machine. This removes the need to propagate the toaster through React context.
const [state, send] = useMachine(
  toast.group.machine({ overlap: false, placement: "bottom" }),
)

const toaster = toast.group.connect(state, send, normalizeProps)

// propagate toaster via React context, then:
toaster.create({ title: "Hello", description: "World" })
For Solid.js users: use the <Key> component exported from @zag-js/solid when mapping over toast items instead of <For>.

Removed

useActor is removed. Use useMachine everywhere instead.
Replaced by the explicit defaultOpen and open props described above.
Removed. Pass count as a prop directly to the machine instead:
const service = useMachine(pagination.machine, {
  count: totalItems,
})
Removed. Pass collection as a prop directly:
const service = useMachine(select.machine, {
  collection,
})

Bug fixes

  • Menu: Fixed context menu not updating positioning on subsequent right-clicks.
  • Avatar: Fixed api.setSrc not working.
  • File Upload: Fixed drag-and-drop failing when directory is true.
  • Carousel: Fixed initial page not being applied correctly. Fixed pagination sync breaking after interacting with dot indicators.

Performance

Zag v1 eliminates the external store in favor of each framework’s native reactive primitives. The following benchmarks measure mount time for 10,000 instances of each component.
# Before
{ phase: "mount",  duration: 1007 ms }
{ phase: "update", duration: 890 ms }

# After
{ phase: "mount",  duration: 737 ms }
{ phase: "update", duration: 2 ms }
# Before
{ phase: "mount",  duration: 2778 ms }
{ phase: "update", duration: 2 ms }

# After
{ phase: "mount", duration: 1079 ms }
# Before
{ phase: "mount",  duration: 834 ms }
{ phase: "update", duration: 2 ms }

# After
{ phase: "mount", duration: 290 ms }
# Before
{ phase: "mount",  duration: 689 ms }
{ phase: "update", duration: 2 ms }

# After
{ phase: "mount", duration: 136 ms }
# Before
{ phase: "mount",  duration: 1680 ms }
{ phase: "update", duration: 2 ms }

# After
{ phase: "mount", duration: 738 ms }
# Before
{ phase: "mount",  duration: 798 ms }
{ phase: "update", duration: 3 ms }

# After
{ phase: "mount", duration: 140 ms }
# Before
{ phase: "mount",  duration: 1414 ms }
{ phase: "update", duration: 0 ms }

# After
{ phase: "mount", duration: 502 ms }
# Before
{ phase: "mount",  duration: 4120 ms }
{ phase: "update", duration: 2014 ms }

# After
{ phase: "mount",          duration: 3880 ms }
{ phase: "nested-update",  duration: 3179 ms }

Bundle size

The @zag-js/core package powers every component. In v1 it is under 2 KB minified — a 98% reduction.
Size
Before13.78 KB
After1.52 KB

Notes for contributors

  • activities is renamed to effects.
  • prop, context, and refs are now passed explicitly to the machine. Previously everything was folded into context, which had a measurable performance cost.
  • The watch syntax has changed significantly. Refer to any built-in machine for the current pattern — it works similarly to useEffect in React, with an explicit dependency-tracking step.
  • createMachine is now an identity function. The machine’s reactive work is performed inside the framework’s useMachine hook rather than at construction time.

Build docs developers (and LLMs) love