Using the Runtime

Lightweight, on-demand CSS engine. It reads your class names directly from the DOM, builds scoped CSS rules, and injects them into the page.

Table of contents

Overview

The runtime is the small JavaScript engine that builds the interactive utility classes. It scans your page, generates only the CSS your classes actually need, and gives you a handful of configuration hooks: debug mode, a safelist for dynamic markup, custom rules, and control over cascade layers. All of it is set through a single window.zRuntime object in your JS.

Debug mode

One deliberate choice worth knowing about: 0build utilities have no graceful fallback, and they also won’t guess. There’s no fallback value quietly filled in when a variable is missing, and no attempt to infer what you probably meant. That’s not an oversight. Automatic fallbacks balloon the code for every single rule, and they trade a visible bug for a quiet one that’s harder to trace. If a variable is missing, the declaration simply doesn’t apply, and debug mode is how you find out.

A typical flow looks like this: build your markup with static classes and inline variables, turn on debug mode while you work, fix whatever it flags, then turn debug mode off before you ship.

During development, do this to surface silent failures that are otherwise easy to miss:

window.zRuntime = window.zRuntime || {};
zRuntime.debug = true;

By default, the boilerplate and Playground already set zRuntime.debug to true. Make sure to turn it off before you hit production.

Debug mode scans every [class] element on the page and warns you if a variable a class needs isn’t actually set.

What it looks like in the console:

[zRuntime] Missing variables (2):
opacity:hover → --opacity-hover, opacity:active → --opacity-active
<div data-fs class="opacity:hover opacity:active">Read More</div>

[zRuntime] Element has interactive state class(es) ["opacity:hover" (state: :hover)]
but is missing the `data-fs` attribute. These styles will have no effect.
<div class="opacity:hover">Read More</div>

Every warning tells you what’s missing, what variable name it expected, and points you straight at the DOM node.

Things it catches for you:

CamelCase vs. kebab-case typos:

<!-- won't match. the runtime looks for a kebab-case name -->
<div class="opacity:hover" style="--opacityHover: 100%"></div>

<!-- this matches -->
<div class="opacity:hover" style="--opacity-hover: 100%"></div>

Partial state coverage, where one variable is present and one is forgotten:

<!-- debug warns: --opacity-active is missing -->
<div class="opacity:hover opacity:active" style="--opacity-hover: 100%"></div>

Debug mode is the safety net you get once your markup is already wired up. If you're outside Playground, that's what you use. Inside the Playground, you also get intellisense checks for the same class-without-variable pairing and vice-versa while you're typing, before you've saved anything.

Safelist

Sometimes a class isn’t in the DOM when the runtime first scans the page. Dynamically injected components, server-rendered HTML, and classes added later through JavaScript are common examples. The safelist tells the runtime to generate CSS for those classes anyway.

Set it before the runtime initializes:

window.zRuntime = window.zRuntime || {};

zRuntime.safelist = [
  "opacity:hover",
  {
    class: "bg",
    states: [":hover", ":focus"],
    prefixes: ["md"],
    dark: true,
  },
];

String form. Each entry is a full class name, parsed exactly as if it were found in the DOM:

zRuntime.safelist = ["opacity:hover", "md:opacity:hover", "dark:opacity:hover"];

Object form. Describe one base class and let the runtime generate every combination of states, breakpoints, and dark/light variants for you:

zRuntime.safelist = [
  {
    class: "opacity",
    states: [":hover", ":focus"],
    prefixes: ["md", "lg"],
    dark: true,
  },
];

// Generates all 12 combinations:
//   opacity:hover          opacity:focus
//   md:opacity:hover       md:opacity:focus
//   lg:opacity:hover       lg:opacity:focus
//   dark:opacity:hover     dark:opacity:focus
//   dark:md:opacity:hover  dark:md:opacity:focus
//   dark:lg:opacity:hover  dark:lg:opacity:focus
PropertyTypeDescription
classstringBase class name (for example opacity)
statesstring[]States to generate (for example [':hover', ':focus'])
prefixesstring[]Breakpoint prefixes (for example ['md', 'lg']). Omit for no prefix.
darkbooleanIf true, also generates every dark-mode variant

Curious what’s queued up? zRuntime.getSafelist() returns the parsed list that will be included in the next generation pass:

console.log(zRuntime.getSafelist());
// [{ baseClass: 'opacity', state: ':hover', fullClass: 'opacity:hover', isDark: false, prefix: null }]

Custom rules

zRuntime.customRules lets you add brand-new utilities or override built-in ones. Set it before the runtime initializes:

window.zRuntime = window.zRuntime || {};

zRuntime.customRules = [
  {
    selector: "ring",
    properties: "box-shadow",
    values: ["0 0 0 3px blue"],
    layer: "utilities",
  },
];
FieldRequiredDescription
selectorYesBase class name this rule matches (for example opacity)
propertiesYesCSS property, or an array of properties
valuesYes (unless arbitrary)Values mapped to each property
arbitraryNotrue means the value comes from a CSS variable, no values needed
placeholdersNoTemplate substitution: parts of values swapped for CSS vars
layerNoCascade layer: components, styles (default), or utilities

If your selector matches a built-in rule, your version replaces it entirely. There’s no partial merging to worry about.

Arbitrary

The value always comes from a variable derived from the class name, state, prefix, and dark flag. You set it per element:

<script>
  window.zRuntime = window.zRuntime || {};

  zRuntime.customRules = [
    {
      selector: "scale",
      properties: "transform",
      arbitrary: true,
      layer: "utilities",
    },
  ];
</script>

<div class="scale:hover" style="--scale-hover: scale(1.05)"></div>

Variable naming convention: --[dark-][prefix-]baseClass-state. For example: --dark-sm-bg-hover.

Placeholders

Template a value where specific parts are swapped for computed CSS variables:

zRuntime.customRules = [
  {
    selector: "shadow",
    properties: "box-shadow",
    values: ["0 0 0 __size__ __color__"],
    placeholders: {
      __size__: "shadow-size",
      __color__: "shadow-color",
    },
  },
];

Each key in placeholders is a substring in values that gets swapped for the computed variable name for that state.

Want to see the full merged rule set, built-ins plus everything you’ve added? zRuntime.getRules() has it:

console.log(zRuntime.getRules());
// [{ selector: 'opacity', properties: 'opacity', values: ['0', '1'], layer: 'styles' }]

Layer

Everything the runtime generates lives inside @layer blocks, controlled per rule through the layer field (default: styles):

@layer components {
  /* rules with layer: 'components' */
}

@layer styles {
  /* default layer */
  .opacity\:hover:hover {
    opacity: var(--opacity-hover);
  }

  @media (min-width: 48rem) {
    .md\:opacity\:hover:hover {
      opacity: var(--opacity-hover);
    }
  }
}

@layer utilities {
  /* rules with layer: 'utilities' */
}

The practical upshot: component styles always yield to utility overrides, and utilities always win, without needing a single !important. Use the layer field in zRuntime.customRules to place your own rules in the right tier.

API reference

MethodWhat it doesWhen to use
zRuntime.refresh()Clears the rule cache and re-runs style generation from scratchAfter adding elements dynamically
zRuntime.regenerate()Re-runs style generation without clearing the cacheWhen your rule definitions haven’t changed (faster than refresh())
zRuntime.getCache()Returns the internal Map of generated rules, keyed by class, state, dark flag, and prefixDebugging/inspection of generated output
zRuntime.getRules()Returns the full merged rule array: built-ins plus your custom rulesInspecting the final rule set used for generation
zRuntime.getSafelist()Returns the parsed safelist entries queued for the next generation passInspecting what’s queued for the next generation pass