A collection of framework-agnostic UI component patterns like accordion, menu, and dialog that can be used to build design systems for React, Vue and Solid.js
Powered by Statecharts
Simple, resilient component logic. Write component logic once and use anywhere.
Accessible
Built-in adapters that connects machine output to DOM semantics in a WAI-ARIA compliant way.
Framework agnostic
Component logic is largely JavaScript code and can be consumed in any JS framework.
Zag machine APIs are completely headless and unstyled. Use your favorite styling solution and get it matching your design system.
import * as numberInput from "@zag-js/number-input" import { useMachine, normalizeProps } from "@zag-js/react" export function NumberInput() { // 1. Consume the machine const [state, send] = useMachine(numberInput.machine({ id: "1" })) // 2. Convert machine to the provided API const api = numberInput.connect(state, send, normalizeProps) // 3. Render the component return ( <div {...api.rootProps}> <label {...api.labelProps}>Enter number:</label> <div> <button {...api.decrementButtonProps}>DEC</button> <input {...api.inputProps} /> <button {...api.incrementButtonProps}>INC</button> </div> </div> ) }
Finite state machines for building accessible design systems and UI components. Works with React, Vue and Solid.
import * as numberInput from "@zag-js/number-input" import { useMachine, normalizeProps } from "@zag-js/react" export function NumberInput() { const [state, send] = useMachine(numberInput.machine({ id: "1" })) const api = numberInput.connect(state, send, normalizeProps) return ( <div {...api.rootProps}> <label {...api.labelProps}>Enter number:</label> <div> <button {...api.decrementTriggerProps}>DEC</button> <input {...api.inputProps} /> <button {...api.incrementTriggerProps}>INC</button> </div> </div> ) }
import * as numberInput from "@zag-js/number-input" import { normalizeProps, useMachine } from "@zag-js/vue" import { computed, defineComponent, h, Fragment } from "vue" export default defineComponent({ name: "NumberInput", setup() { const [state, send] = useMachine(numberInput.machine({ id: "1" })) const apiRef = computed(() => numberInput.connect(state.value, send, normalizeProps), ) return () => { const api = apiRef.value return ( <div {...api.rootProps}> <label {...api.labelProps}>Enter number</label> <div> <button {...api.decrementTriggerProps}>DEC</button> <input {...api.inputProps} /> <button {...api.incrementTriggerProps}>INC</button> </div> </div> ) } }, })
<script setup> import * as numberInput from "@zag-js/number-input"; import { normalizeProps, useMachine } from "@zag-js/vue"; import { computed } from "vue"; const [state, send] = useMachine(numberInput.machine({ id: "1" })); const api = computed(() => numberInput.connect(state.value, send, normalizeProps) ); </script> <template> <div ref="ref" v-bind="api.rootProps"> <label v-bind="api.labelProps">Enter number</label> <div> <button v-bind="api.decrementTriggerProps">DEC</button> <input v-bind="api.inputProps" /> <button v-bind="api.incrementTriggerProps">INC</button> </div> </div> </template>
import * as numberInput from "@zag-js/number-input" import { normalizeProps, useMachine } from "@zag-js/solid" import { createMemo, createUniqueId } from "solid-js" export function NumberInput() { const [state, send] = useMachine(numberInput.machine({ id: createUniqueId() })) const api = createMemo(() => numberInput.connect(state, send, normalizeProps)) return ( <div {...api().rootProps}> <label {...api().labelProps}>Enter number:</label> <div> <button {...api().decrementTriggerProps}>DEC</button> <input {...api().inputProps} /> <button {...api().incrementTriggerProps}>INC</button> </div> </div> ) }
We need a better way to model component logic.Zag is a new approach to the component design process, designed to help you avoid re-inventing the wheel and build better UI components regardless of framework. Heavily inspired by XState, but built to make it easier to maintain, test, and enhance.
Creator of Zag.js