@zag-js/core vs angular vs react vs vue
Frontend Architecture: UI Frameworks and Component Logic Layers
@zag-js/coreangularreactvueSimilar Packages:

Frontend Architecture: UI Frameworks and Component Logic Layers

angular, react, and vue are comprehensive UI frameworks used to build entire web applications, managing rendering, routing, and state. @zag-js/core is different — it is a framework-independent logic layer that handles complex interactive component behavior (like menus, dialogs) using state machines, often used inside the other three. While the frameworks decide how your app looks and updates the screen, Zag decides how complex widgets behave and stay accessible.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
@zag-js/core0-67.7 kB-10 days agoMIT
angular058,5062.09 MB461-MIT
react0250,670179 kB1,37314 days agoMIT
vue054,4212.55 MB8927 days agoMIT

Frontend Architecture: UI Frameworks and Component Logic Layers

When architecting a modern web application, you typically choose a UI framework to handle rendering and overall structure. However, for complex interactive widgets, you might also need a dedicated logic layer. angular, react, and vue are full frameworks for building applications. @zag-js/core is a utility for managing component state machines. Let's compare how they handle state, accessibility, and rendering.

🏗️ Core Responsibility: Rendering vs. Logic

react, vue, and angular are responsible for rendering UI to the screen. They manage the DOM, handle updates, and structure your application.

// react: Renders JSX to DOM
function Button() {
  return <button>Click Me</button>;
}
// vue: Renders Template to DOM
// <template><button>Click Me</button></template>
export default { name: 'Button' };
// angular: Renders Template to DOM
// @Component({ template: '<button>Click Me</button>' })
export class ButtonComponent {}

@zag-js/core does not render anything. It provides the logic (state machine) that you connect to your framework of choice.

// zag: Provides logic, no rendering
import { createMachine } from '@zag-js/core';

const machine = createMachine({
  id: 'button',
  context: { pressed: false },
  states: { idle: {}, pressed: {} }
});

🧠 State Management: Reactivity vs. State Machines

react uses local state hooks that trigger re-renders when changed. It is simple but can get complex for multi-step interactions.

// react: useState hook
const [isOpen, setIsOpen] = useState(false);
const toggle = () => setIsOpen(prev => !prev);

vue uses reactive references that update the view automatically when the value changes.

// vue: ref reactive state
const isOpen = ref(false);
const toggle = () => isOpen.value = !isOpen.value;

angular uses signals (in modern versions) to track state changes and update the view efficiently.

// angular: signal state
const isOpen = signal(false);
const toggle = () => isOpen.update(v => !v);

@zag-js/core uses a state machine pattern. You send events to the machine, and it transitions states based on defined rules. This prevents invalid states.

// zag: Machine transitions
const service = interpret(machine);
service.start();
service.send({ type: 'TOGGLE' }); // Transitions based on logic

♿ Accessibility: Manual vs. Built-in

react, vue, and angular require you to manually manage ARIA attributes and keyboard interactions for complex widgets. This is error-prone.

// react: Manual ARIA management
<button 
  aria-expanded={isOpen} 
  onClick={toggle}
>
  Menu
</button>
// vue: Manual ARIA management
<button 
  :aria-expanded="isOpen" 
  @click="toggle"
>
  Menu
</button>
// angular: Manual ARIA management
// <button [attr.aria-expanded]=

How to Choose: @zag-js/core vs angular vs react vs vue

  • @zag-js/core:

    Choose @zag-js/core when you need to build complex, accessible interactive widgets like comboboxes or date pickers without managing the state machine logic yourself. It is perfect for design system teams who want to write the logic once and use it across React, Vue, or Angular projects.

  • angular:

    Choose angular if you are building large-scale enterprise applications that benefit from a strict, opinionated structure. It includes built-in solutions for HTTP, forms, and routing, reducing the need to select third-party libraries. It is best for teams that value long-term stability and strong TypeScript integration.

  • react:

    Choose react if you want maximum flexibility and access to the largest ecosystem of third-party tools. It is ideal for projects where you want to choose your own routing, state management, and styling solutions. It works well for teams comfortable with JavaScript-heavy patterns and hooks.

  • vue:

    Choose vue if you prefer a progressive framework that is easy to start with but scales well. It offers a balanced approach with single-file components and a clear separation of concerns. It is suitable for teams wanting a gentle learning curve without sacrificing power in complex applications.

README for @zag-js/core

@zag-js/core

This package contains a minimal implementation of XState FSM for finite state machines with addition of extra features we need for our components.

Features

  • Finite states with optional nested (hierarchical) states
  • Initial state
  • Transitions (object or strings)
  • Context
  • Entry actions
  • Exit actions
  • Transition actions
  • Effects
  • Boolean guard helpers (and, or, not)

Note: The core package is intentionally minimal. It doesn't include delayed transitions, interval scheduling, spawn helpers, or activities. Use actions/effects to perform side effects.

To better understand the state machines, we strongly recommend going though the xstate docs and videos. It'll give you the foundations you need.

Quick start

Installation

npm i @zag-js/core
# or
yarn add @zag-js/core

Usage (machine):

import { createMachine } from "@zag-js/core"

const toggleMachine = createMachine({
  id: "toggle",
  initialState() {
    return "inactive"
  },
  states: {
    inactive: { on: { TOGGLE: "active" } },
    active: { on: { TOGGLE: "inactive" } },
  },
})

Usage (service via adapter):

import { createMachine } from "@zag-js/core"
import { VanillaMachine } from "@zag-js/vanilla"

const machine = createMachine({
  initialState() {
    return "inactive"
  },
  states: {
    inactive: { on: { TOGGLE: "active" } },
    active: { on: { TOGGLE: "inactive" } },
  },
})

const service = new VanillaMachine(machine)
service.start()

service.subscribe(() => {
  console.log(service.state.get())
})

service.send({ type: "TOGGLE" })
service.send({ type: "TOGGLE" })
service.stop()

Nested states (dot notation)

import { createMachine } from "@zag-js/core"

const machine = createMachine({
  initialState() {
    return "dialog"
  },
  states: {
    dialog: {
      tags: ["overlay"],
      initial: "closed",
      states: {
        closed: { on: { OPEN: { target: "dialog.open" } } },
        open: { on: { CLOSE: { target: "dialog.closed" } } },
      },
      // parent-level transitions still work
      on: { RESET: { target: "dialog.closed" } },
    },
  },
})

// service.state.matches("dialog.open") === true when nested state is active

State IDs and #id targets

Use id on a state node when you want to target it explicitly from anywhere in the machine.

import { createMachine } from "@zag-js/core"

const machine = createMachine({
  initialState() {
    return "dialog"
  },
  states: {
    dialog: {
      initial: "open",
      states: {
        focused: {
          id: "dialogFocused",
        },
        open: {
          initial: "idle",
          states: {
            idle: {
              on: {
                CLOSE: { target: "#dialogFocused" },
              },
            },
          },
        },
      },
    },
  },
})

Notes:

  • #id targets resolve by state node id (XState-style).
  • State IDs must be unique within a machine.

API

createMachine(config, options)

Creates a new finite state machine from the config.

ArgumentTypeDescription
configobject (see below)The config object for creating the machine.
optionsobject (see below)The optional options object.

Returns:

A machine definition object consumed by framework adapters (React, Solid, Svelte, Vue, Preact, Vanilla) to create a runtime service.

The machine config has this schema:

Machine config

  • initialState (function) - returns the machine's initial state value.
  • states (object) - mapping of state names to state configs. Supports nested states and initial for child states.
  • on (object) - optional global transitions available from any state.
  • context (function) - returns bindable context fields.
  • computed (object) - derived values based on context, props, refs.
  • entry / exit / effects - root-level actions/effects.
  • implementations (object) - lookup tables for actions, guards, effects.

State config

  • on (object) - an object mapping event types (keys) to transitions

Transition config

String syntax:

  • (string) - the state name to transition to.
    • Same as { target: stateName }

Object syntax:

  • target (string) - the state name to transition to.
  • actions (Action | Action[]) - the action(s) to execute when this transition is taken.
  • guard (Guard) - the condition (predicate function) to test. If it returns true, the transition will be taken.

Machine options

  • actions? (object) - a lookup object for your string actions.
  • guards? (object) - a lookup object for your string guards specified as guard in the machine.
  • activities? (object) - a lookup object for your string activities.
  • delays? (object) - a lookup object for your string delays used in after and every config.

Action config

The action function to execute while transitioning from one state to another. It takes the following arguments:

  • context (any) - the machine's current context.
  • event (object) - the event that caused the action to be executed.