Skip to main content
In many real-world scenarios you need to drive a machine’s state from outside code — setting an initial value, keeping a controlled value in sync with external state, or calling API methods imperatively. Zag supports all of these patterns.

Setting an initial value

All machines accept a default* prop for setting an uncontrolled initial value. The machine owns the state from that point forward; you simply seed it.
// Accordion opens with "item-1" expanded by default
const service = useMachine(accordion.machine, {
  defaultValue: ["item-1"],
})
Similar default* props exist across all components:
PropUsed by
defaultValueAccordion, Select, Combobox, Slider, Number Input, Tags Input, …
defaultOpenDialog, Popover, Tooltip, Hover Card, Collapsible, …
defaultCheckedCheckbox, Radio Group, Switch

Controlled usage

Pass the current value directly and respond to changes via a callback. The machine will reflect the value you provide and call the callback whenever an interaction would change it.
function App() {
  const [value, setValue] = React.useState(["item-1"])

  const service = useMachine(accordion.machine, {
    value,
    onValueChange(details) {
      setValue(details.value)
    },
  })

  // ...
}
When you provide a controlled value, the machine will not update it internally. You are responsible for updating it in the onValueChange callback.

Reactive context

Different frameworks handle reactivity differently. The sections below show the idiomatic approach for each supported framework.
React props are natively reactive when passed directly to useMachine. No extra wrapping is needed.
import { useMachine, normalizeProps } from "@zag-js/react"
import * as checkbox from "@zag-js/checkbox"

function Checkbox(props) {
  const service = useMachine(checkbox.machine, {
    checked: props.checked,
    onCheckedChange: props.onCheckedChange,
  })

  const api = checkbox.connect(service, normalizeProps)
  return <label {...api.getRootProps()}>Toggle</label>
}
How reactive context works:
  1. The function re-evaluates whenever its dependencies change, keeping the machine current with the latest prop values.
  2. The machine’s internal state is preserved — only the context configuration is refreshed.
  3. Each framework’s reactivity model drives the re-evaluation, so it integrates naturally with the rest of your application.

Using API methods

The connect function returns an API object with imperative methods for reading and updating machine state. This is the recommended way to trigger changes from outside the normal event flow.
function Accordion() {
  const service = useMachine(accordion.machine, { id: useId() })
  const api = accordion.connect(service, normalizeProps)

  function openFirst() {
    // Imperatively expand item-1
    api.setValue(["item-1"])
  }

  return (
    <>
      <button onClick={openFirst}>Open first item</button>
      <div {...api.getRootProps()}>
        {/* accordion items */}
      </div>
    </>
  )
}
API methods vary by component. Refer to each component’s documentation for the full list. Common examples include:
MachineExample methods
Accordionapi.setValue(values), api.getItemState(props)
Dialogapi.setOpen(open)
Selectapi.selectValue(value), api.clearValue()
Sliderapi.setValue(value)
Tags Inputapi.addValue(value), api.clearValue()

Sending events directly

For lower-level control, you can send events to the machine’s state machine directly via service.send. This is useful when building custom machines or when an API method does not exist for your use case.
const service = useMachine(accordion.machine, { id: useId() })

// Send an event directly to the machine
service.send({ type: "VALUE.SET", value: ["item-2"] })
Prefer the connect API methods over service.send when they exist. Direct event sending bypasses the type-safe API surface and couples your code to internal event names that may change between versions.

Build docs developers (and LLMs) love