Skip to content

Type to search the docs, API, and blog.

↑↓ navigate↵ openesc close
Zazz 0.7 alpha
Docs navigation

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 liveUpdatesBest for
CLIin your repo (zazz/)three-way merge with the published versionapps you will customize and keep
CDNon jsDelivr, pinnedchange a version stringprototypes, docs, CMS pages
npmin node_modulespnpm updatebundled 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 dialog

init 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/core
import "@zazz-ui/core/index.css";
import "@zazz-ui/core"; // the behavior

The 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>