Lightweight, on-demand CSS engine. It reads your class names directly from the DOM, builds scoped CSS rules, and injects them into the page.
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.
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.
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| Property | Type | Description |
|---|---|---|
class | string | Base class name (for example opacity) |
states | string[] | States to generate (for example [':hover', ':focus']) |
prefixes | string[] | Breakpoint prefixes (for example ['md', 'lg']). Omit for no prefix. |
dark | boolean | If 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 }]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",
},
];| Field | Required | Description |
|---|---|---|
selector | Yes | Base class name this rule matches (for example opacity) |
properties | Yes | CSS property, or an array of properties |
values | Yes (unless arbitrary) | Values mapped to each property |
arbitrary | No | true means the value comes from a CSS variable, no values needed |
placeholders | No | Template substitution: parts of values swapped for CSS vars |
layer | No | Cascade 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.
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.
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' }]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.
| Method | What it does | When to use |
|---|---|---|
zRuntime.refresh() | Clears the rule cache and re-runs style generation from scratch | After adding elements dynamically |
zRuntime.regenerate() | Re-runs style generation without clearing the cache | When 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 prefix | Debugging/inspection of generated output |
zRuntime.getRules() | Returns the full merged rule array: built-ins plus your custom rules | Inspecting the final rule set used for generation |
zRuntime.getSafelist() | Returns the parsed safelist entries queued for the next generation pass | Inspecting what’s queued for the next generation pass |