What is a state machine?
What is a state machine?
A finite state machine is a model of computation where a system can be in exactly one of a finite number of states at any given time. Transitions between states are triggered by events and can carry side effects (called actions and effects).In the context of UI components, state machines make implicit behavior explicit. Instead of tracking
isOpen, isFocused, isAnimating, and isDisabled as separate booleans and reasoning about their combinations, you define named states — idle, open, focused, disabled — and declare exactly which events cause transitions between them.This produces component logic that is predictable, testable, and easy to debug. Because the valid states and transitions are defined up front, impossible states (e.g. simultaneously open and disabled) simply cannot be reached.Why not use XState?
Why not use XState?
Zag was inspired by XState and shares its foundational ideas, but it does not implement the full SCXML specification. Instead, Zag uses a simpler, purpose-built API optimized for the specific patterns common in UI components.The Zag machine format avoids complex machine concepts like
spawn, deeply nested states, and parallel state nodes. The goal is to keep machines light-weight and easy to understand at a glance. This trade-off suits headless component libraries well — the behavior space of a tooltip or a dialog is well-defined enough that full statechart expressiveness is rarely needed.If you are already using XState for application-level state and want to use Zag for component-level logic, both can coexist in the same project.Can I use Zag without a framework (vanilla JS)?
Can I use Zag without a framework (vanilla JS)?
Yes. The machine packages (
@zag-js/tooltip, @zag-js/dialog, etc.) have no dependency on any framework. You can import and run them directly in any JavaScript environment.The framework adapters (@zag-js/react, @zag-js/vue, etc.) are optional conveniences that integrate useMachine with a framework’s reactivity system and provide the correct normalizeProps implementation.In a vanilla JS context you would start the machine manually, subscribe to state changes, and apply props to DOM elements yourself. This is the same model the framework adapters use internally — they just automate the subscription and re-render step.How do I style components?
How do I style components?
Zag is completely headless. The
connect function returns props and state — it never applies inline styles, CSS classes, or any visual presentation.You can use any styling approach:- CSS classes — Spread the returned props and add your own
classNamealongside them. - Data attributes — Zag sets
data-*attributes on elements (e.g.data-open,data-disabled,data-highlighted) that you can target with CSS selectors. - Tailwind CSS — Combine Tailwind utility classes with the data attribute selectors Zag sets.
- CSS-in-JS — Style the component using any runtime styling library.
Does Zag work with server-side rendering (SSR)?
Does Zag work with server-side rendering (SSR)?
Yes. The machine packages contain no DOM references at import time. Machines are started inside framework lifecycle hooks (
useEffect in React, onMounted in Vue, onMount in Solid) so they never run during server rendering.The React adapter is marked "use client" for compatibility with React Server Components. Non-interactive server components can freely import types and static utilities from Zag packages without triggering client-side execution.Is Zag accessible?
Is Zag accessible?
Accessibility is a primary design goal, not an afterthought. All component machines are modeled according to the WAI-ARIA Authoring Practices. The
connect function returns the correct ARIA roles, states, and properties for every element.Each machine also implements the expected keyboard interaction patterns defined by WAI-ARIA — arrow key navigation in listboxes, Escape to close dialogs, roving tabindex in radio groups, and so on.End-to-end tests are written for every component and run against all supported frameworks using Playwright. The test suite verifies that keyboard and pointer interactions work correctly regardless of which framework is used.How is Zag different from Radix UI or Headless UI?
How is Zag different from Radix UI or Headless UI?
All three libraries are headless component primitives focused on accessibility, but they differ in architecture and scope.Framework coupling — Radix UI targets React only. Headless UI supports React and Vue with separate packages. Zag separates the component logic from the framework entirely, so the same machine runs in React, Vue, Solid, and Svelte without duplication.State machines — Radix and Headless UI manage state with standard framework primitives (
useState, ref, etc.). Zag uses an explicit state machine model, which makes the component’s behavior fully enumerable and testable independently of any framework.Styling API — All three are unstyled. Radix and Headless UI expose data attributes. Zag also exposes data attributes and follows the same pattern.If you are building a cross-framework design system or a component library that must work in more than one framework, Zag’s architecture avoids duplicating interaction logic for each target.Why do I need normalizeProps?
Why do I need normalizeProps?
The
connect function in each machine package produces props using a normalized internal format. normalizeProps converts those props into the specific format that each framework requires before you spread them onto DOM elements.There are subtle but real differences between frameworks:- Event handler casing — React and Solid use
onKeyDown; Vue usesonKeydown. - Inline style values — React accepts numeric pixel values (
{ marginBottom: 4 }); Solid requires CSS property names with string units ({ "margin-bottom": "4px" }); Vue requires string values with units ({ marginBottom: "4px" }).
normalizeProps from the adapter for your framework (@zag-js/react, @zag-js/vue, etc.) and pass it to connect. The machine handles the rest.normalizeProps also ensures that the props returned by connect are strongly typed to the correct element attribute types for your framework.What frameworks does Zag support?
What frameworks does Zag support?
Zag currently provides official adapters for:
- React (
@zag-js/react) — React 18+ - Vue (
@zag-js/vue) — Vue 3 - Solid (
@zag-js/solid) — Solid.js - Svelte (
@zag-js/svelte) — Svelte 5
How do I attach custom event handlers alongside Zag's props?
How do I attach custom event handlers alongside Zag's props?
Because Zag returns plain props objects, you can merge your own handlers with the ones returned by Most
connect. The recommended pattern is to compose the event handlers so both run:get*Props functions accept an optional props argument that is merged with the machine’s own props before being returned.