How it works

The mental model behind NibCSS: from an attribute to rules, classes and a stylesheet — and why the output is deterministic.

2 min readv1

You can use NibCSS without knowing its internals, but a correct mental model makes every other page obvious. It fits in five steps.

  1. 1 · PARSE
    Split the attribute on ; into rules.
  2. 2 · RESOLVE
    Expand recipes, replace $tokens, map shorthands.
  3. 3 · HASH
    Name each rule nib-<hash>.
  4. 4 · EMIT
    Write the CSS once — to a file or a <style>.
  5. 5 · APPLY
    Put the classes on the element.

1. Parse#

An attribute is a list of rules separated by ;. Each rule is [modifiers:]name[=value][!important]:

Text
nib="p=16px; md:hover:bgc=#111; btn-primary"
       ───┬───  ─────┬───── ──┬── ─┬── ──────┬────
        rule   modifiers  name value  component (no "=")

2. Resolve#

  • A name without = is a component recipe from your config and is expanded into its own rules.
  • $tokens in values are replaced: bgc=$color.brand → bgc=#4F52D9.
  • The name is looked up in the ~290 built-in shorthands (bgc → background-color), your own shortcuts, or accepted as a real CSS property (caret-color, --my-var). Anything else is reported and dropped.

3. Hash#

The normalised rule is hashed into a class name. The same rule always produces the same name:

Text
p=16px          →  .nib-jbmcpl   { padding: 16px }
mx=auto         →  .nib-1yxik1w  { margin-left: auto; margin-right: auto }
dark:cl=#fff    →  .dark .nib-jalsyg { color: #fff }

4. Emit#

Each distinct rule is written once, however many elements use it. Base rules come first and media-query blocks after them, so a responsive rule always overrides its mobile default.

5. Apply#

The element receives its class names — at build time (rewrite: true) or by the runtime in the browser.

Build time or run time?#

The same five steps run in two places:

Build time (@nibcss/unplugin, CLI)

Run time (@nibcss/runtime)

Static nib="p=16px"

✅ extracted into a .css file

✅ scanned and applied

Dynamic values (useNib(…) with state)

—

✅ generated on the fly

JavaScript needed in the browser

none (with rewrite: true)

~23 kB

First paint

styled (render-blocking <link>)

styled (see No flash)

They compose: static markup costs nothing at runtime, and only truly dynamic values use the runtime.

Why deterministic class names matter#

Because a class name is a pure function of its rule:

  • Server and client agree. SSR output hydrates without mismatches.
  • Caching works. The stylesheet only changes when a new rule appears.
  • Diffs are stable. Reordering components does not rename classes.
  • No duplication. 500 elements with p=16px share one rule.

One class per rule, one class per recipe#

A utility produces one class. A component recipe produces one class holding all of its declarations — even across modifiers:

CSS
/* nib="card-hover" where card-hover = "card; hover:tf=translateY(-4px)" */
.nib-1nfp073 { padding: 24px; border-radius: 12px }
.dark .nib-1nfp073 { background-color: #111 }
.nib-1nfp073:hover { transform: translateY(-4px) }

Next steps#