Zag is distributed as independent npm packages — one per component machine, plus a small adapter for each framework. This guide walks through installing and using the tooltip machine as a concrete example. The same pattern applies to every other machine in the library.
Install the packages
You need two packages:
- A component machine — the framework-agnostic interaction logic (e.g.
@zag-js/tooltip)
- A framework adapter — integrates the machine with your framework’s reactivity system
Install a component machine
Pick the machine for the component you want to use. This example uses @zag-js/tooltip.npm install @zag-js/tooltip
Install the framework adapter
Install the adapter that matches your framework.npm install @zag-js/react
Using the machine
All frameworks follow the same two-step pattern: start the machine with useMachine, then connect it to your component tree using the machine’s connect function.
import * as tooltip from "@zag-js/tooltip"
import { useMachine, normalizeProps } from "@zag-js/react"
export function Tooltip() {
const service = useMachine(tooltip.machine, { id: "my-tooltip" })
const api = tooltip.connect(service, normalizeProps)
return (
<>
<button {...api.getTriggerProps()}>Hover me</button>
{api.open && (
<div {...api.getPositionerProps()}>
<div {...api.getContentProps()}>Tooltip content</div>
</div>
)}
</>
)
}
import * as tooltip from "@zag-js/tooltip"
import { normalizeProps, useMachine } from "@zag-js/vue"
import { computed, defineComponent, Fragment } from "vue"
export default defineComponent({
name: "Tooltip",
setup() {
const service = useMachine(tooltip.machine, { id: "my-tooltip" })
const api = computed(() => tooltip.connect(service, normalizeProps))
return () => {
const { getTriggerProps, getPositionerProps, getContentProps, open } = api.value
return (
<>
<button {...getTriggerProps()}>Hover me</button>
{open && (
<div {...getPositionerProps()}>
<div {...getContentProps()}>Tooltip content</div>
</div>
)}
</>
)
}
},
})
import * as tooltip from "@zag-js/tooltip"
import { normalizeProps, useMachine } from "@zag-js/solid"
import { createMemo, createUniqueId, Show } from "solid-js"
export function Tooltip() {
const service = useMachine(tooltip.machine, { id: createUniqueId() })
const api = createMemo(() => tooltip.connect(service, normalizeProps))
return (
<div>
<button {...api().getTriggerProps()}>Hover me</button>
<Show when={api().open}>
<div {...api().getPositionerProps()}>
<div {...api().getContentProps()}>Tooltip content</div>
</div>
</Show>
</div>
)
}
<script lang="ts">
import * as tooltip from "@zag-js/tooltip"
import { useMachine, normalizeProps } from "@zag-js/svelte"
const service = useMachine(tooltip.machine, { id: "my-tooltip" })
const api = $derived(tooltip.connect(service, normalizeProps))
</script>
<button {...api.getTriggerProps()}>Hover me</button>
{#if api.open}
<div {...api.getPositionerProps()}>
<div {...api.getContentProps()}>Tooltip content</div>
</div>
{/if}
Understanding normalizeProps
normalizeProps is a required argument to every connect call. It converts the props returned by the machine into the exact format each framework expects.
There are subtle differences in how element attributes and event handlers are named across frameworks. For example:
Event handler casing
| Framework | Keydown listener property |
|---|
| React, Solid | onKeyDown |
| Vue | onKeydown |
Inline styles
| Framework | Style value for margin |
|---|
| React | { marginBottom: 4 } (number) |
| Solid | { "margin-bottom": "4px" } |
| Vue | { marginBottom: "4px" } (string with unit) |
These differences are handled automatically when you import normalizeProps from the correct framework adapter. You never need to think about them.
Always import normalizeProps from the same package as useMachine — for example, @zag-js/react for React projects. Each adapter exports the normalizer appropriate for that framework.
Upgrading all Zag packages
Zag uses independent versioning for each package. To upgrade all @zag-js/* packages at once, use your package manager’s scoped upgrade command: