Skip to main content

Registry

ZUI components ship two ways. Both give you the same code; they differ in who owns it afterwards.

npm or registry

npm (@zuilib/primitives)Registry (source)
Installpnpm add @zuilib/primitivesnpx @zuilib/cli add button
Importimport Button from '@zuilib/primitives/button'import Button from '@/components/zui/button'
Code lives innode_modulesyour repo (components/zui/)
Upgradesbump the versionzui update and review the diff
Customisationoverride tokens on :root or a subtreeoverride tokens, or edit the file
Build requirementnone (zui.css is prebuilt)Tailwind v4 in your app

Pick npm when you want a dependency: one line to upgrade, no code to maintain, styling through the token contract only. Pick the registry when the component must become yours: you intend to change its markup or behaviour, or your organization forbids runtime UI dependencies. Mixing is fine. A project can install @zuilib/tokens from npm and pull only the components it wants to fork from the registry.

Only @zuilib/primitives is in the registry (with the token CSS it depends on). @zuilib/data-grid, charts, ai, apps and text-editor are npm packages.

Set up

Your project needs Tailwind v4. Run once, in the project root:

npx @zuilib/cli init

That writes zui.json, installs the token css and the stylesheet entry under styles/zui/ (src/styles/zui/ when the project has a src/ folder) and adds one line to your global stylesheet, after @import "tailwindcss":

@import "tailwindcss";
@import "./styles/zui/zui.css";

Dark mode is the dark class on <html>, exactly as with the npm package. init guesses the directories; --components, --styles, --css override them, and everything is editable in zui.json afterwards:

{
"registry": {
"url": "https://zuilib.com/r",
"version": "latest"
},
"paths": {
"components": "src/components/zui",
"styles": "src/styles/zui",
"css": "src/index.css"
}
}

Set registry.version to a release ("0.1.0") to pin — init --registry-version 0.1.0 writes it: every add and update then reads /r/v/0.1.0/ and nothing moves until you change it.

Add a component

npx @zuilib/cli add button

That writes components/zui/button.tsx, then follows the item's registryDependencies: the helpers it imports land in components/zui/lib/ (the same tree as the registry, so the relative imports in the source work unchanged), and npm dependencies the component needs (@headlessui/react, clsx, tailwind-merge, @floating-ui/react) are installed with your package manager at the versions ZUI is built against. Several items in one go:

npx @zuilib/cli add input select dialog

Every write is recorded twice: zui.lock.json gets the file's sha256, and .zui/snapshots/<item>/<target> gets a pristine copy of the registry content. Commit both. The lock tells a file you edited from one the registry changed; the snapshot is what update merges against, and a checkout without them has no upgrade path. add --dry-run prints what would be written and installed without touching anything. add refuses to overwrite an installed file you edited; add --force overwrites it and keeps <file>.orig.

The npm dependencies an item declares are validated before anything is spawned (name, @scope/name, optional @range; nothing that looks like a flag), and --strict fails before any file is written; the package manager runs without a shell, after the files and the lock are written. A package already in your package.json is never reinstalled; if its range sits on another major than the item needs, add warns and --strict fails.

Keeping up to date

npx @zuilib/cli list # ✓ installed, ↑ newer in the registry
npx @zuilib/cli diff # unified diff, installed files vs registry
npx @zuilib/cli status # drift table; --json for CI, --strict exits 1 on drift
npx @zuilib/cli update # merge the registry's changes into your files

diff compares your files against the registry as it is now (latest, or the pinned version), not against the copy the lock was written from. update decides per file:

Your copyRegistryupdate
unchangedmovedrewrites it
editedunchangedleaves it (unchanged)
editedmovedthree-way merge: .zui/snapshots vs yours vs the registry

The merge is line based. Hunks that only one side changed are taken from that side; a hunk both sides changed differently is written with git-style markers:

<<<<<<< ours
return 11
=======
return 12
>>>>>>> zui@0.2.0

After a merge the file holds the result, <file>.orig your pre-merge copy, .zui/snapshots the new registry content, and the lock the new version. Conflicted files are listed as conflict and the command exits 1; resolve the markers and commit. update refuses to merge a file that still has markers. update --dry-run prints would merge / conflict per file without writing. update --force skips the merge and overwrites edited files (keeping <file>.orig). Keeping your changes in tokens (:root overrides) rather than in the component files keeps conflicts rare.

status shows the outcome before you run update: its update column is rewrite, merge-needed, conflict-likely or no-snapshot. no-snapshot marks an item installed by a CLI older than .zui/snapshots; zui snapshot restore seeds the directory (unedited files from disk, edited ones from /r/v/<locked version>/ of the registry the lock recorded), and until then update refuses to touch that item's edited files.

npx @zuilib/cli remove button # deletes the item's files, lock entry and snapshot
npx @zuilib/cli remove button --prune # also the helpers nothing else installed still needs

remove refuses an edited file without --force (which keeps <file>.orig).

Pinning is per registry: registry.version in zui.json (and registries.<name>.version for a named one) freezes the whole registry at a release. There is no per-item pin; to hold one item back, leave it out of zui update <item>….

Offline and integrity

Every fetched document is cached under .zui/cache with its ETag (the directory ignores itself in git) and revalidated with If-None-Match on the next run. --offline on any command reads only from the cache and fails on a url never fetched.

A registry index may carry integrity, the sha256 of its compact json without that field. The CLI recomputes it on every fetch and refuses a mismatch. zui registry add <name> <url> --pin copies the current value into zui.json (registries.<name>.integrity); zui registry pin does the same for the default registry (registry.integrity), zui registry pin <name> for a configured one, and either moves the pin after a change you trust. From then on an index that hashes differently is refused until you change the pin. A pin covers that registry's own index only, not the registries it extends. This is trust on first use, not a signature chain: it catches a registry that changed under you, not a registry that was wrong from the start.

What is in the registry

ItemTypeContents
<component> (button, select, table, …)componentOne component file, target components/zui/<name>.tsx
lib-<helper> (lib-cn, lib-control-frame, …)libOne shared helper, target components/zui/lib/<helper>.ts
zui-tokensthemeThe token contract: light and dark custom properties, keyframes, animations, the Tailwind @theme mapping and base styles
zui-stylesthemeThe stylesheet entry (styles/zui/zui.css) plus the component css Tailwind cannot express

The registry is static json, no server behind it:

UrlContents
/r/latest/index.jsonEvery item: type, version, description, npm dependencies, registry dependencies, file targets — and, per component, props (name, type, default, allowed values, one-line description, parsed from the README's API table) so a tool can offer or validate a component's contract without reading its source
/r/latest/button.jsonOne item with its file contents and their sha256; an item can be inspected without installing it
/r/v/<version>/…The same, frozen per @zuilib/primitives release
/r/schema.jsonJSON schema of the index and item documents

Tokens alone, for a project that only wants the design contract:

npx @zuilib/cli add zui-tokens

Organizational registries

The same mechanism scales to a company: a design-system team builds its own registry (its composites, its themed overrides, its tokens) with zui registry build, hosts it as static files, and declares that it extends https://zuilib.com/r. Product teams run zui registry add acme <url> once and then zui add acme/data-table; dependencies resolve in the org registry first and fall through to ZUI, an org button overrides ZUI's for every component that imports it, and zui status --json reports drift across repos. Private hosting works with ZUI_REGISTRY_TOKEN_<NAME>. The full walkthrough and config reference: Organizational registries.

For coding agents: llms.txt

The site publishes two files in the llms.txt format so an agent can learn the library without crawling the docs:

  • /llms.txt: the site map. One line per doc page, one line per package, one line per component with its import line and purpose, one line per helper and theme item, each linking to its registry json.
  • /llms-full.txt: the full API reference in one file — the @zuilib/primitives setup, import rules, every component's props, slots and conventions, then the README of every other package (@zuilib/ai, charts, data-grid, text-editor, views) and the complete token contract.

Point an agent at https://zuilib.com/llms.txt in its instructions (CLAUDE.md, AGENTS.md, .cursorrules) and it can pick the right component, write the correct import and know which tokens to override. Give it llms-full.txt when it needs prop-level detail offline; for structured prop metadata (a picker UI, validation), read props from the registry index instead. An agent with shell access installs components the same way you do: npx @zuilib/cli add <item>.

Building the registry

Everything under /r/, llms.txt and llms-full.txt is generated from packages/primitives/src, packages/tokens/src and the package READMEs by packages/primitives/scripts/build-registry.mjs. It resolves each component's relative imports into registryDependencies and its package imports into dependencies, hashes every file, writes /r/latest/ and /r/v/<version>/, and fails when an import cannot be matched to an item (every import of ZUI's own components must resolve inside the registry; the zui registry build an organization runs only warns, because its unmatched imports are usually provided by the registry it extends). The READMEs are enforced build inputs: a component missing its row in the components README's Exports table or its Component API section fails the build, so descriptions and props can never silently fall back. Run it from the repo root:

pnpm registry:build

pnpm build and pnpm docs:build run it too, so the deployed site never carries a stale registry. Deploying the registry is deploying the docs site.