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:
| Prop | Used by |
|---|
defaultValue | Accordion, Select, Combobox, Slider, Number Input, Tags Input, … |
defaultOpen | Dialog, Popover, Tooltip, Hover Card, Collapsible, … |
defaultChecked | Checkbox, 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>
}
Wrap the context in a computed ref so Vue can track its dependencies:<script setup lang="ts">
import { computed } from "vue"
import { useMachine, normalizeProps } from "@zag-js/vue"
import * as checkbox from "@zag-js/checkbox"
const props = defineProps<{
checked: boolean
onCheckedChange: (details: any) => void
}>()
const service = useMachine(
checkbox.machine,
computed(() => ({
checked: props.checked,
onCheckedChange: props.onCheckedChange,
})),
)
</script>
Pass a function that returns the context. Solid’s reactive system will track reads inside the function.import { useMachine, normalizeProps } from "@zag-js/solid"
import * as checkbox from "@zag-js/checkbox"
import { createMemo } from "solid-js"
function Checkbox(props) {
const service = useMachine(checkbox.machine, () => ({
checked: props.checked,
onCheckedChange: props.onCheckedChange,
}))
const api = createMemo(() => checkbox.connect(service, normalizeProps))
return <label {...api().getRootProps()}>Toggle</label>
}
Use Svelte’s $state rune and pass a function that returns the context:<script lang="ts">
import { useMachine, normalizeProps } from "@zag-js/svelte"
import * as checkbox from "@zag-js/checkbox"
let { checked = false, onCheckedChange } = $props()
const service = useMachine(checkbox.machine, () => ({
checked,
onCheckedChange(details) {
checked = details.checked
onCheckedChange?.(details)
},
}))
const api = $derived(checkbox.connect(service, normalizeProps))
</script>
<label {...api.getRootProps()}>Toggle</label>
How reactive context works:
- The function re-evaluates whenever its dependencies change, keeping the machine current with the latest prop values.
- The machine’s internal state is preserved — only the context configuration is refreshed.
- 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:
| Machine | Example methods |
|---|
| Accordion | api.setValue(values), api.getItemState(props) |
| Dialog | api.setOpen(open) |
| Select | api.selectValue(value), api.clearValue() |
| Slider | api.setValue(value) |
| Tags Input | api.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.