npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@magnet-js/zag

v0.1.1

Published

ZagJS integration for Magnet.

Readme

@magnet/zag

ZagJS integration for Magnet. Bridges ZagJS framework-agnostic state machines to Magnet signals and wraps them as Magnet components.

Install

JSR

deno add jsr:@magnet/zag

npm via JSR

npx jsr add @magnet/zag

npm

npm install @magnet-js/zag

How it works

ZagJS ships state machines (machine) and API connectors (connect) per component. createService runs a machine in a framework-agnostic service, publishes every transition into a writable signal, and re-derives the connected API through a computed signal. Component wrappers bind the API's prop getters to Magnet tag functions, so element attributes and event handlers update reactively.

Adding another ZagJS machine only requires calling createService with the component's machine and connect exports.

API

createService

Starts a ZagJS machine and bridges its state to signals. The signal argument only needs state and computed primitives; a Magnet context satisfies the interface directly.

import { tc39 } from "@magnet/signal";
import { createService } from "@magnet/zag";
import * as accordion from "@zag-js/accordion";

const z = createService(tc39, accordion.machine, accordion.connect, {
  id: "accordion-1",
});

z.api.get(); // connected API, re-derived on every transition
z.send({ type: "VALUE.SET", value: ["a"] }); // send events to the machine
z.service.get(); // raw ZagJS service snapshot signal
z.stop(); // stop the machine and detach synchronization

Any ZagJS component package (@zag-js/tabs, @zag-js/dialog, …) works the same way.

normalizeProps

Normalizer passed to ZagJS connect functions so the emitted props match Magnet's attribute model: onXyz handlers are lowercased to Magnet event listener keys, alias props are mapped (htmlForfor, classNameclass), style objects become CSS strings, and boolean aria-* values become "true"/"false" strings. createService applies it automatically.

accordion

import { magnet } from "@magnet/ui";
import { tc39 } from "@magnet/signal";
import { accordion } from "@magnet/zag";

const m = magnet({ window, ...tc39 });
const { root, item, itemTrigger, itemContent } = accordion(m, {
  id: "faq",
  collapsible: true,
});

const faq = root([
  item({ value: "a" })([
    itemTrigger({ value: "a" })(["Question A"]),
    itemContent({ value: "a" })(["Answer A"]),
  ]),
]);

dialog

const { trigger, content, title, description, closeTrigger } = dialog(m, {
  id: "confirm",
  modal: true,
});

const open = trigger(["Open"]);
const panel = content([
  title(["Confirm"]),
  description(["Are you sure?"]),
  closeTrigger(["Close"]),
]);

Overlay parts work inline — the machine finds them by id — but you can portal them to any container with Magnet.render, for example to escape an overflow: hidden ancestor by rendering into document.body:

m.render(document.body, [
  backdrop(),
  positioner([panel]),
]);

The cleanup returned by render (or the component's own stop cleanup) tears the portal down with the component.

tabs

const { root, list, trigger, content } = tabs(m, {
  id: "settings",
  defaultValue: "general",
});

const settings = root([
  list([
    trigger({ value: "general" })(["General"]),
    trigger({ value: "advanced" })(["Advanced"]),
  ]),
  content({ value: "general" })(["General settings"]),
  content({ value: "advanced" })(["Advanced settings"]),
]);

Components

Every wrapper follows the pattern shown above: one machine per call, a service object (api, send, service, stop), and pre-bound part factories. The full catalog, grouped by purpose:

  • Disclosure: accordion, collapsible, tabs, treeView
  • Forms and input: angleSlider, cascadeSelect, checkbox, colorPicker, combobox, dateInput, datePicker, editable, fileUpload, numberInput, passwordInput, pinInput, radioGroup, ratingGroup, select, signaturePad, slider, switch, tagsInput, toggle, toggleGroup
  • Overlays: dialog, drawer, floatingPanel, hoverCard, popover, tooltip, tour
  • Menus and navigation: menu, navigationMenu, pagination, steps
  • Feedback and display: avatar, carousel, clipboard, imageCropper, listbox, marquee, progress, qrCode, scrollArea, splitter, timer, toastGroup

Two naming notes: the toast wrapper is exported as toastGroup (one service manages the whole group), and switch is a reserved word — import it with an alias (import { switch as switch_ } from "@magnet/zag"). The menu wrapper covers nested submenus; coordinated menubar behavior lives one layer up, in @magnet/shadcn's menubarGroup.

Parts and attributes

Part factories mirror the Magnet tag call signature and accept optional extra attributes, which are merged over the ZagJS props:

root({ class: "faq" }, [/* ... */]);

Every wrapper returns the underlying ZagService (api, send, service, stop) alongside its part factories. Every member — part factories, api, send, and stop — is a plain closure, so the whole service can be safely destructured, as the examples above do.

Lifecycle

Each component wrapper starts one ZagJS machine, which keeps running until its stop() is called. Machine lifetime is the caller's responsibility, and the idiomatic way to manage it is Magnet's own cleanup channel: children arrays accept cleanup functions, which run when the component's subtree is disposed. Return stop as one more child:

const settingsTabs = (m: Magnet<Context<MagnetWindow>>) => {
  const { root, list, stop } = tabs(m, { defaultValue: "general" });
  return [
    root([
      list([/* ... */]),
      // ...
    ]),
    stop, // runs when settingsTabs' subtree is disposed
  ];
};

stop is a plain closure over the running machine, so passing it bare (destructured off the service object, as above) is safe.

A few properties of this model, and why it works the way it does:

  • Disposal, not disconnection. Cleanup functions run when the render machinery discards the subtree — not when elements are temporarily disconnected. This is why stop belongs in a cleanup function rather than in a _use action: _use fires on every connection change, and stopping on a temporary disconnect would leave remounted elements wired to a dead machine.
  • Re-mounts start fresh. When a computed child re-renders a component, the component function runs again from scratch: new elements, new machine, new stop cleanup. There is no "stale machine behind live DOM" state in the standard pattern.
  • Unreachable machines are collectible. If every reference to the service and its elements is dropped, the machine, signals, and DOM nodes become garbage together — stop() is about prompt teardown, not about avoiding leaks.
  • Detached subtrees stay alive on purpose. Magnet keeps removed-by-signal subtrees reachable, and their machines keep running with them — consistent with the framework's memory model. Note the edge this implies: if you hold on to a component's element objects and re-insert the identical nodes after they were unmounted (and their cleanups ran), those elements are backed by a stopped machine. Mount components by calling their functions instead.

License

MIT © 2026 Fernando G. Vilar.