Docs
Installation
Own the files with the CLI, load them from a CDN, or install the package with a bundler.
Zazz installs three ways. All produce the same kit; they differ in who owns the files.
| Files live | Updates | Best for | |
|---|---|---|---|
| CLI | in your repo (zazz/) | three-way merge with the published version | apps you will customize and keep |
| CDN | on jsDelivr, pinned | change a version string | prototypes, docs, CMS pages |
| npm | in node_modules | pnpm update | bundled apps that treat the kit as a dependency |
CLI
The zazz-ui CLI copies primitives from @zazz-ui/core into your project so you own the code, and records where every file came from so later updates merge instead of overwrite.
pnpm dlx zazz-ui init
pnpm dlx zazz-ui add button card dialoginit vendors the base platform into zazz/: the layers, tokens, reset, typography, the generated utilities, the core runtime scripts, an index.css and index.js pair, and a head.html snippet to paste into your <head>. It also writes zazz.json, the kit version plus a hash of every vendored file's original bytes. Flags: --dir <path>, --ts (TypeScript sources instead of emitted JavaScript), --legacy <path> (wire an existing stylesheet into the legacy.imports sublayer), --no-fonts (omit the Geist block from head.html), --no-theme-script.
add <primitive…> resolves dependencies (each primitive's API page lists what its command brings; combobox pulls in six), copies the files at your recorded version, and inserts the imports at the right cascade position. --examples also copies the example fragments.
update [@version] [names…] moves the project, or the named primitives, to a new version. Because zazz.json knows which files you edited, untouched files take the new version silently, your edits survive where upstream did not move, and non-overlapping changes merge. On a real conflict the CLI asks you to choose: write conflict markers, keep yours, take theirs, or skip (--markers, --keep, --theirs answer for non-interactive runs). The relevant slice of the changelog prints first, breaking changes flagged. diff shows the same read-only.
There is no runtime dependency on the package. Edit the files, delete what you do not need, commit them.
CDN
Two requests load the whole kit from jsDelivr: the single-file stylesheet and script. This is the <head> the kit's buildHead() produces for the installed version:
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Paint the right theme (background, form controls, scrollbars) before CSS loads -->
<meta name="color-scheme" content="light dark">
<!-- Zazz styles: the whole kit, one request -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/dist/zazz.css">
<!-- Import map: bare specifiers in Zazz scripts resolve to pinned, SRI-checked files -->
<script type="importmap">
{
"imports": {
"signal-polyfill": "https://cdn.jsdelivr.net/npm/signal-polyfill@0.2.2/dist/index.js",
"embla-carousel": "https://cdn.jsdelivr.net/npm/embla-carousel@8.6.0/esm/embla-carousel.esm.js",
"embla-carousel-autoplay": "https://cdn.jsdelivr.net/npm/embla-carousel-autoplay@8.6.0/esm/embla-carousel-autoplay.esm.js",
"embla-carousel-auto-scroll": "https://cdn.jsdelivr.net/npm/embla-carousel-auto-scroll@8.6.0/esm/embla-carousel-auto-scroll.esm.js",
"embla-carousel-class-names": "https://cdn.jsdelivr.net/npm/embla-carousel-class-names@8.6.0/esm/embla-carousel-class-names.esm.js"
},
"integrity": {
"https://cdn.jsdelivr.net/npm/signal-polyfill@0.2.2/dist/index.js": "sha384-0OqM7OXGT6Oz1z98u/geiFn/FUyz4iWexkXHCvbIGXRRDXl6bp6IKc0j78AQ6DEK",
"https://cdn.jsdelivr.net/npm/embla-carousel@8.6.0/esm/embla-carousel.esm.js": "sha384-GXjfyU1fSHHeweoYd3rwo7OBKYtkq+JcsDoe8VDRc6znCty2ZTzyVvX1DwqCE+9t",
"https://cdn.jsdelivr.net/npm/embla-carousel-autoplay@8.6.0/esm/embla-carousel-autoplay.esm.js": "sha384-AX49yBc7zd/CT8ZqNMRiiqqVSV9xfxb27VwEGziPioT3wL4k4C5BW2qwefdWuN4J",
"https://cdn.jsdelivr.net/npm/embla-carousel-auto-scroll@8.6.0/esm/embla-carousel-auto-scroll.esm.js": "sha384-SPdnL1eic73WfM72zFEf0RJ5PdUC7TL3Yb5jOl1JC3/l4D7EsVxwO8wFnnezlMSu",
"https://cdn.jsdelivr.net/npm/embla-carousel-class-names@8.6.0/esm/embla-carousel-class-names.esm.js": "sha384-ZWno/FqzYeWE5Ils19GIhep1AZ5bMl0mp9wmm6baTJE4+pjMjOdTnE55SjUQAdVh"
}
}
</script>
<link rel="modulepreload" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/dist/zazz.js">
<!-- Polyfill: Interest Invokers (interestfor) — native in Chromium only -->
<script type="module" src="https://cdn.jsdelivr.net/npm/invokers@2.2.2/dist/esm/production/interest.js" integrity="sha384-hR2BVNtsS7fIIMwOm+f7MY0JZfuByGhQQfevCBB6evFATKzBsTw0JzH/RxD94z2T" crossorigin="anonymous"></script>
<!-- Zazz behavior: the whole kit, one module -->
<script type="module" src="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/dist/zazz.js"></script>
<!-- Theme: apply the persisted (or preferred) scheme before first paint -->
<script>
(() => {
try {
// A stored choice pins the scheme with the theme-dark / theme-light
// switch; with no choice the page follows the system scheme.
const storedTheme = localStorage.getItem("theme");
if (storedTheme === "dark" || storedTheme === "light") {
const root = document.documentElement;
root.dataset.ui = ((root.dataset.ui || "") + " theme-" + storedTheme).trim();
}
} catch {
/* no storage access: follow the system scheme */
}
})();
</script>To load only the primitives a page uses, name them. The kit's manifest resolves each primitive's dependencies, and every URL is pinned and carries an integrity hash:
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Paint the right theme (background, form controls, scrollbars) before CSS loads -->
<meta name="color-scheme" content="light dark">
<!-- Zazz styles: base layers, then fields, card, kbd, button, dialog in cascade order -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_layers.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_variables.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_breakpoints.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_reset.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_typography.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_view-transitions.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/primitives/fields/fields.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/primitives/card/card.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/primitives/kbd/kbd.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/primitives/button/button.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/primitives/dialog/dialog.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_properties.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_properties-sm.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_properties-md.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_properties-lg.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_properties-xl.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_properties-2xl.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_properties-disabled.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_properties-active.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_properties-focus-visible.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_properties-focus-within.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_properties-hover.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_properties-checked.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_properties-current.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_properties-open.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_properties-stuck.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_properties-starting.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_utilities-tier-sm.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_utilities-tier-md.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_utilities-tier-lg.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_utilities-tier-xl.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_utilities-tier-2xl.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_utilities-tier-disabled.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_utilities-tier-active.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_utilities-tier-focus-visible.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_utilities-tier-focus-within.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_utilities-tier-hover.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_utilities-tier-checked.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_utilities-tier-current.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_utilities-tier-open.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_utilities-tier-stuck.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_utilities-tier-starting.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_utilities-flow.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_utilities-grid.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_utilities-spacing.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_utilities-margin.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_utilities-sizing.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_utilities-typography.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_utilities-color.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_utilities-effects.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_utilities-box.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_utilities-pseudo.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_defaults.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/_switches.css">
<!-- Import map: bare specifiers in Zazz scripts resolve to pinned, SRI-checked files -->
<script type="importmap">
{
"imports": {
"signal-polyfill": "https://cdn.jsdelivr.net/npm/signal-polyfill@0.2.2/dist/index.js",
"embla-carousel": "https://cdn.jsdelivr.net/npm/embla-carousel@8.6.0/esm/embla-carousel.esm.js",
"embla-carousel-autoplay": "https://cdn.jsdelivr.net/npm/embla-carousel-autoplay@8.6.0/esm/embla-carousel-autoplay.esm.js",
"embla-carousel-auto-scroll": "https://cdn.jsdelivr.net/npm/embla-carousel-auto-scroll@8.6.0/esm/embla-carousel-auto-scroll.esm.js",
"embla-carousel-class-names": "https://cdn.jsdelivr.net/npm/embla-carousel-class-names@8.6.0/esm/embla-carousel-class-names.esm.js"
},
"integrity": {
"https://cdn.jsdelivr.net/npm/signal-polyfill@0.2.2/dist/index.js": "sha384-0OqM7OXGT6Oz1z98u/geiFn/FUyz4iWexkXHCvbIGXRRDXl6bp6IKc0j78AQ6DEK",
"https://cdn.jsdelivr.net/npm/embla-carousel@8.6.0/esm/embla-carousel.esm.js": "sha384-GXjfyU1fSHHeweoYd3rwo7OBKYtkq+JcsDoe8VDRc6znCty2ZTzyVvX1DwqCE+9t",
"https://cdn.jsdelivr.net/npm/embla-carousel-autoplay@8.6.0/esm/embla-carousel-autoplay.esm.js": "sha384-AX49yBc7zd/CT8ZqNMRiiqqVSV9xfxb27VwEGziPioT3wL4k4C5BW2qwefdWuN4J",
"https://cdn.jsdelivr.net/npm/embla-carousel-auto-scroll@8.6.0/esm/embla-carousel-auto-scroll.esm.js": "sha384-SPdnL1eic73WfM72zFEf0RJ5PdUC7TL3Yb5jOl1JC3/l4D7EsVxwO8wFnnezlMSu",
"https://cdn.jsdelivr.net/npm/embla-carousel-class-names@8.6.0/esm/embla-carousel-class-names.esm.js": "sha384-ZWno/FqzYeWE5Ils19GIhep1AZ5bMl0mp9wmm6baTJE4+pjMjOdTnE55SjUQAdVh"
}
}
</script>
<!-- Polyfill: Interest Invokers (interestfor) — native in Chromium only -->
<script type="module" src="https://cdn.jsdelivr.net/npm/invokers@2.2.2/dist/esm/production/interest.js" integrity="sha384-hR2BVNtsS7fIIMwOm+f7MY0JZfuByGhQQfevCBB6evFATKzBsTw0JzH/RxD94z2T" crossorigin="anonymous"></script>
<!-- Zazz behavior: side-effect modules by tag; the rest via module imports -->
<script type="module" src="https://cdn.jsdelivr.net/npm/@zazz-ui/core@0.7.0/src/base/dialog-lifecycle.js"></script>
<!-- Theme: apply the persisted (or preferred) scheme before first paint -->
<script>
(() => {
try {
// A stored choice pins the scheme with the theme-dark / theme-light
// switch; with no choice the page follows the system scheme.
const storedTheme = localStorage.getItem("theme");
if (storedTheme === "dark" || storedTheme === "light") {
const root = document.documentElement;
root.dataset.ui = ((root.dataset.ui || "") + " theme-" + storedTheme).trim();
}
} catch {
/* no storage access: follow the system scheme */
}
})();
</script>npm
pnpm add @zazz-ui/coreimport "@zazz-ui/core/index.css";
import "@zazz-ui/core"; // the behaviorThe package also exports the single-file bundles (@zazz-ui/core/zazz.css, @zazz-ui/core/zazz.js), individual files (@zazz-ui/core/primitives/dialog/dialog.css, @zazz-ui/core/base/signals.js), the editor data (@zazz-ui/core/editor/*), and the head builder (@zazz-ui/core/head). Bundlers resolve the kit's bare dependencies (signal-polyfill and four Embla packages) from node_modules, so no import map is needed.
The head contract
Whichever route you take, a Zazz page's <head> has the same parts, in this order: the meta tags, any font links, the stylesheet link(s), the import map, a modulepreload hint, the interest-invokers polyfill, the module script, and the theme script. buildHead() from @zazz-ui/core/head writes it; the CLI's head.html is that output; this site's own head is too. The theme script must stay inline and last so the theme-* switch lands before first paint. Under a strict Content Security Policy, the two inline scripts take a nonce (buildHead({ nonce })) and the style attribute needs style-src-attr 'unsafe-inline'; the security page has the policy and what the kit does without it.
For a vendored copy at ./zazz/:
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Paint the right theme (background, form controls, scrollbars) before CSS loads -->
<meta name="color-scheme" content="light dark">
<!-- Zazz styles: one bundle (index.css @imports every layer in cascade order) -->
<link rel="stylesheet" href="./zazz/index.css">
<!-- Import map: bare specifiers in Zazz scripts resolve to pinned, SRI-checked files -->
<script type="importmap">
{
"imports": {
"signal-polyfill": "https://cdn.jsdelivr.net/npm/signal-polyfill@0.2.2/dist/index.js",
"embla-carousel": "https://cdn.jsdelivr.net/npm/embla-carousel@8.6.0/esm/embla-carousel.esm.js",
"embla-carousel-autoplay": "https://cdn.jsdelivr.net/npm/embla-carousel-autoplay@8.6.0/esm/embla-carousel-autoplay.esm.js",
"embla-carousel-auto-scroll": "https://cdn.jsdelivr.net/npm/embla-carousel-auto-scroll@8.6.0/esm/embla-carousel-auto-scroll.esm.js",
"embla-carousel-class-names": "https://cdn.jsdelivr.net/npm/embla-carousel-class-names@8.6.0/esm/embla-carousel-class-names.esm.js"
},
"integrity": {
"https://cdn.jsdelivr.net/npm/signal-polyfill@0.2.2/dist/index.js": "sha384-0OqM7OXGT6Oz1z98u/geiFn/FUyz4iWexkXHCvbIGXRRDXl6bp6IKc0j78AQ6DEK",
"https://cdn.jsdelivr.net/npm/embla-carousel@8.6.0/esm/embla-carousel.esm.js": "sha384-GXjfyU1fSHHeweoYd3rwo7OBKYtkq+JcsDoe8VDRc6znCty2ZTzyVvX1DwqCE+9t",
"https://cdn.jsdelivr.net/npm/embla-carousel-autoplay@8.6.0/esm/embla-carousel-autoplay.esm.js": "sha384-AX49yBc7zd/CT8ZqNMRiiqqVSV9xfxb27VwEGziPioT3wL4k4C5BW2qwefdWuN4J",
"https://cdn.jsdelivr.net/npm/embla-carousel-auto-scroll@8.6.0/esm/embla-carousel-auto-scroll.esm.js": "sha384-SPdnL1eic73WfM72zFEf0RJ5PdUC7TL3Yb5jOl1JC3/l4D7EsVxwO8wFnnezlMSu",
"https://cdn.jsdelivr.net/npm/embla-carousel-class-names@8.6.0/esm/embla-carousel-class-names.esm.js": "sha384-ZWno/FqzYeWE5Ils19GIhep1AZ5bMl0mp9wmm6baTJE4+pjMjOdTnE55SjUQAdVh"
}
}
</script>
<link rel="modulepreload" href="./zazz/index.js">
<!-- Polyfill: Interest Invokers (interestfor) — native in Chromium only -->
<script type="module" src="https://cdn.jsdelivr.net/npm/invokers@2.2.2/dist/esm/production/interest.js" integrity="sha384-hR2BVNtsS7fIIMwOm+f7MY0JZfuByGhQQfevCBB6evFATKzBsTw0JzH/RxD94z2T" crossorigin="anonymous"></script>
<!-- Zazz behavior: one ES module imports every component script in dependency order -->
<script type="module" src="./zazz/index.js"></script>
<!-- Theme: apply the persisted (or preferred) scheme before first paint -->
<script>
(() => {
try {
// A stored choice pins the scheme with the theme-dark / theme-light
// switch; with no choice the page follows the system scheme.
const storedTheme = localStorage.getItem("theme");
if (storedTheme === "dark" || storedTheme === "light") {
const root = document.documentElement;
root.dataset.ui = ((root.dataset.ui || "") + " theme-" + storedTheme).trim();
}
} catch {
/* no storage access: follow the system scheme */
}
})();
</script>