Zag: headless component state machines for React, Vue, Solid and Svelte
Build your design system in React, Solid, Vue, Svelte or Vanilla. Powered by finite state machines
At a glance
- What is it?
- Zag implements common component patterns as framework-agnostic finite state machines, with thin adapters for React, Vue, Solid, Svelte and vanilla JS. It is a good fit if you maintain a design system across more than one framework, and a poor fit if you want finished, styled components.
- Who is it for?
- Adopt Zag if you maintain component patterns across two or more frameworks, or if you want the interaction logic of a dialog or menu separated from its rendering and covered by Playwright tests against the WAI-ARIA spec. Do not adopt it if you want finished, styled components, or if your team will not read machine definitions when something misbehaves.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The duplication problem Zag was built to remove
The README states the problem directly: with the rise of design systems and component-driven development, there is an endless re-implementation of the same patterns (Tabs, Menu, Modal) in multiple frameworks. Those implementations are similar in spirit; the differences sit in each framework's reactivity and effects system, so useState and useEffect in React versus the equivalents elsewhere. Framework-specific solutions grow in complexity over time and become hard to understand, debug, improve or test.
The target reader is therefore not someone building a single application. It is a design system team, or a library author, who has to ship the same dialog, tooltip or menu to React, Vue, Solid and Svelte consumers. Zag's answer is to model the interaction once, in a framework-agnostic machine, and publish small framework wrappers around it. The README lists @zag-js/react, @zag-js/vue, @zag-js/solid and @zag-js/svelte as those wrappers, and the repository also carries a vanilla example. If you only ever target one framework, the abstraction buys you little.
How a Zag machine, service and connect call fit together
Zag is not a component library in the usual sense. Each component is published as its own package, for example @zag-js/dialog or @zag-js/tooltip, and exports a machine. The README describes the machine APIs as completely unstyled, leaving the styling solution to you.
The React usage example shows the flow. useMachine from @zag-js/react takes the result of calling the component's machine with a configuration object and returns a service. That service is passed to the component's connect function together with normalizeProps, which produces an api object. You then spread api.getRootProps() onto your own element and api.getItemProps({ value }) onto each child. The machine owns state and event handling; connect maps that state onto props you attach to whatever markup you write. That is the whole contract, and it is why the same machine can back four frameworks: the framework layer only supplies a service and a props normalizer.
The README gives the id as a required piece of machine configuration, using React's useId in the example. The guiding principles state that machines are modelled according to the WAI-ARIA authoring practices, that end-to-end tests are written for every component based on that spec, and that machines should stay light-weight and avoid complex machine concepts like spawn and nested states. The README also says Zag does not follow the SCXML specification, and credits XState as the inspiration for the base implementation.
Installing Zag and wiring a toggle group in React
The README gives the install step as a per-component package. Replace {component} with the machine you need, for example dialog or tooltip.
npm i --save @zag-js/{component}The README also shows the yarn equivalent, yarn add @zag-js/{component}. For a React app you additionally need the React adapter, so a toggle group install looks like this.
npm i --save @zag-js/toggle-group @zag-js/reactThe README's usage example is a complete component. It imports normalizeProps and useMachine from @zag-js/react, imports the toggle group machine namespace, and calls useId from React to supply the machine id. The service goes through connect, and the resulting api supplies getRootProps for the container and getItemProps for each button.
import { normalizeProps, useMachine } from "@zag-js/react"
import * as toggle from "@zag-js/toggle-group"
import { useId } from "react"
export function ToggleGroup() {
const service = useMachine(toggle.machine({ id: useId() }))
const api = toggle.connect(service, normalizeProps)
return (
<div {...api.getRootProps()}>
<button {...api.getItemProps({ value: "bold" })}>B</button>
<button {...api.getItemProps({ value: "italic" })}>I</button>
<button {...api.getItemProps({ value: "underline" })}>U</button>
</div>
)
}What you should see is three unstyled buttons with no visual grouping, because Zag ships no CSS. Keyboard behaviour, aria roles and attributes come from the machine. If you want to see a machine running inside a real app before writing one, the repository has starter projects: the package.json defines start-react for examples/next-ts, start-vue for examples/nuxt-ts, start-solid, start-svelte, start-preact and start-vanilla. The README also lists e2e-react, e2e-vue and e2e-solid as Playwright scripts, with pw-test defaulting FRAMEWORK to react.
Where Zag is the wrong tool
Zag gives you logic, not a product. Every visual decision, every class name and every layout rule is yours, so a team that wants a styled dialog out of the box will write more code with Zag than with a themed component library. The README is explicit that the machine APIs are unstyled.
The second cost is conceptual. Adopting Zag means your team reads state machine definitions when a component misbehaves, not just component code. The README's guiding principles deliberately keep machines light and avoid spawn and nested states, which limits how much machine theory you need, but the mental model is still different from a hook that returns booleans. A team that has never worked with statecharts should budget for that.
The third limitation is that the README documents the pattern, not the catalogue. It shows one toggle group example and points to zagjs.com for the rest. There is no prop table, no per-component keyboard reference and no migration guide in the README itself, so evaluating whether @zag-js/menu covers your submenu requirements means reading the package source. The README also notes that Zag does not follow the SCXML specification and that the statechart API is the maintainers' own design, so prior SCXML or XState experience transfers only partly.
Finally, the release list shows 2.0.0-next.3 prereleases dated 2026-09-14 for @zag-js/vue, @zag-js/virtualizer and @zag-js/vanilla. If your policy forbids next-tagged dependencies, check which version line you are actually installing before you build on it. The README does not document rollback or downgrade steps.
Zag against Radix UI and the rest of the headless field
The README credits Radix UI for inspiring the dismissable and presence pattern, and the two projects solve overlapping problems from different directions. Radix UI is a headless component library for React. Zag is a headless component library whose logic layer is framework-agnostic and whose React package is one adapter among several. If your codebase is React only, Radix gives you the same unstyled-primitives model without an extra indirection layer between the machine and your component.
The practical difference shows up in a monorepo that ships to React and Vue. With Radix you would either maintain two implementations or accept that only one framework gets the components. With Zag the machine package is shared and each framework gets a wrapper, which is the duplication the README opens by describing. The trade is that you depend on Zag's adapter for your framework staying current: the 2.0.0-next.3 releases cover Vue and vanilla, and the README lists React, Vue, Solid and Svelte wrappers without stating a support policy for any of them.
The README's inspiration list also names Material Components Web, Thought on Pure UI by Guillermo Rauch, Pure UI Control by Adam Solve, Vue.js and Lit for the computed and watch patterns, and Sonner for the toast machine. Those references are worth reading if you want to understand why the machine API is shaped the way it is.
Licence and the cost of tracking a monorepo
Zag is MIT licensed, both in the repository metadata and in the root package.json, which sets license to MIT. That permits commercial use and modification under the usual MIT terms; it is not legal advice, and if you vendor or fork the machines you should read the LICENSE file at the repository root yourself.
The upgrade cost is structural rather than financial. The root package.json shows a pnpm workspace with turborepo and esbuild driving the build, a changeset directory for releases, and renovate.json for dependency updates. Consumers install individual @zag-js packages, so upgrades arrive per component rather than as one bundle. That is convenient when you only use a dialog, and awkward when a shared internal utility changes: you can end up with @zag-js/dialog on one version and @zag-js/tooltip on another, and the README does not state a compatibility guarantee across component packages. The 2.0.0-next.3 prereleases also mean the project is publishing a next line alongside the stable one, so pinning exact versions in your lockfile is the safer default.
The maintenance signal is straightforward. The repository is not archived and the last push was on 2026-09-22, one day before this review, so the codebase is moving. That says nothing about whether any single machine meets your accessibility bar; the README's claim is that end-to-end tests exist for every component based on the WAI-ARIA spec, and the way to check that claim for the component you need is to read its tests in the repository.
Editorial conclusion
Adopt Zag if you maintain component patterns across two or more frameworks, or if you want the interaction logic of a dialog or menu separated from its rendering and covered by Playwright tests against the WAI-ARIA spec. Do not adopt it if you want finished, styled components, or if your team will not read machine definitions when something misbehaves. Before committing, open the source of the one machine you need most and confirm that its props, keyboard handling and focus behaviour match your product requirements, because the README documents the pattern rather than the per-component API.
Frequently asked questions
What is Zag and how does it relate to Chakra UI?
Zag is a set of finite state machines for accessible JavaScript components, published as per-component packages such as @zag-js/dialog and @zag-js/tooltip. It lives in the chakra-ui GitHub organization, and the README lists duplicate code in Chakra UI's React and Vue implementations as one of the motivations for the project.
Which frameworks does Zag support?
The README lists adapters for React (@zag-js/react), Vue (@zag-js/vue), Solid (@zag-js/solid) and Svelte (@zag-js/svelte), and the repository also carries a vanilla TypeScript example alongside Next, Nuxt, Preact, Solid and Svelte starters.
Does Zag ship styled components?
No. The README describes the machine APIs as completely unstyled and says Zag gives you control over which styling solution you use, so you attach the props returned by connect to your own markup and write the CSS yourself.
How do I install Zag for a React project?
The README's install step is npm i --save @zag-js/{component}, where {component} is the machine you need, plus the adapter package for your framework, for example @zag-js/react. The usage example then imports normalizeProps and useMachine from the adapter and calls connect on the machine service.
Is Zag tested across frameworks?
The README states that end-to-end tests are written for every machine and that Playwright is used to ensure a component works the same way regardless of framework. The root package.json defines e2e-react, e2e-vue, e2e-solid, e2e-svelte and e2e-preact scripts.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/chakra-ui-zag)