No flash of unstyled content

How NibCSS guarantees a styled first paint in every setup — and the two habits that break it.

1 min readv1

A stylesheet produced by JavaScript has one hard constraint: anything applied after the browser's first paint is a visible flash. NibCSS avoids it with a different mechanism in each setup.

With a bundler#

Static attributes are extracted into a real .css file, loaded as a render-blocking <link> — exactly like a hand-written stylesheet. There is nothing left to flash. With rewrite: true the elements do not even need the runtime.

From a CDN#

Put the script in <head>, with no defer, async or type="module". It starts observing before <body> is parsed and applies classes at the microtask checkpoint — which runs before the browser's "update the rendering" step. Elements are styled in the same turn the parser creates them.

Don't

Why

<script defer>

runs after parsing — the page has already painted

<script async>

may run after paint

<script type="module">

modules are deferred by default

the tag at the end of <body>

everything above it has already painted

In a client-rendered app#

Elements do not exist until the framework renders them. The adapters resolve classes synchronously during render, so a node carries its class the first time it is committed.

With SSR / SSG#

Render the collected dynamic CSS into the HTML, and the client runtime adopts that same <style id="nib"> element — no duplicate, no gap:

TSX
const rt = new NibRuntime({ watch: false, ...config });
const body = renderToString(<NibProvider runtime={rt}><App /></NibProvider>);
const html = `<head><style id="nib">${collectNibStyles(rt)}</style></head><body>${body}</body>`;

Two habits that bring the flash back#

  1. Deferring the CDN script. See the table above.
  2. Using a recipe name as a class. className="btn" bypasses the engine; there is no .btn. Use nib="btn" or useNib("btn").

Next steps#