Navigation

Fundamentals

Rendering modes

Choose where to generate CSS, compare static, pre-render, runtime and progressive delivery, and verify the result.

Choose when CSS is generated and how it reaches the page.

On this page

#Overview

Choose a rendering mode by when your application knows its complete class names. Start with static rendering when those names are visible in source. Use pre-rendering to generate CSS from finished page HTML, runtime rendering for new classes created in the browser, or progressive rendering to combine page CSS with runtime support.

Complete your framework's installation guide first. Official integrations load the CSS entry and configure the selected mode; the examples below explain what each mode does and how to verify it. Framework support and the HTML available for processing depend on that integration.

#Choose a rendering mode

ModeClasses come fromInitial CSSMaster CSS in the browser
StaticComplete strings in source filesA generated stylesheetNo runtime or engine
Pre-renderRendered page HTMLRules delivered with that pageNo runtime or engine
RuntimeConnected DOM elementsRules generated after runtime startupRuntime and engine
ProgressiveRendered HTML, then later DOM changesRules delivered with that pageRuntime tracks usage; the engine can wait until a class is missing

An interactive application can use static CSS. Switching among known classes, changing CSS variables, and using native selectors do not require a runtime engine. Runtime generation matters when a complete class name first becomes available after the build.

Pre-rendering requires actual page markup. An empty root element in a client-rendered app gives the server renderer no application classes to process. Also check the integration's boundary: the Next.js progressive adapter processes build-time HTML, so request-time dynamic SSR responses rely on the browser runtime for generated rules.


#Static rendering

The build scans source text for complete class strings and generates CSS from the project's definitions. It does not execute application code or observe the DOM. The browser loads the resulting stylesheet without a Master CSS runtime.

#Generated stylesheet entry

Import Master CSS from the stylesheet your application already loads:

src/style.css
@import '@master/css';

This import is the insertion point for generated utilities, their required resources, and the preset's base styles. Use one entry for each generated stylesheet root. Keep it in the top-level import block, before ordinary style rules; other CSS imports in that block are emitted before generated Master CSS.

To see the source-to-CSS process, add this utility definition after the import and use the button in a scanned source file:

@utility btn {  display: inline-flex;  padding-inline: var(--spacing-md);}
<button class="btn text-center">Save</button>
Generated CSS
@layer theme {  :root,  :host {    --spacing-md: 1rem  }}@layer utilities {  .btn {    display: inline-flex;    padding-inline: var(--spacing-md)  }  .text-center {    text-align: center  }}

The scanner finds btn and text-center. The engine resolves the project recipe and the preset's fixed utility, then generates their rules and referenced resources. Defining btn alone does not emit its rule: a scanned class or explicit safelist must request it.

Integrations discover ordinary application sources and update CSS during development. Keep complete class names in source and pass changing values through CSS variables; Scanning class names covers both, plus @source and safelist exceptions.

#Native CSS entries

Native selectors in the CSS entry are preserved by default, even when their classes are absent from scanned source. Their var(--*) and animation references can retain the matching theme variables and keyframes.

Use pruneNativeCSS: true when you intend to prune project-owned native CSS, or @prune native; for one file. @preserve native; excludes a file from integration-level pruning. See pruning unused native CSS.


A late class cannot extend a static stylesheet

This example assumes p-lg was absent from all build inputs and safelists. Dynamic values can use custom properties without changing classes.

Text description

A static build sees p-md and emits its rule. A later browser change introduces p-lg, which has no rule in that output.

#Runtime rendering

Use runtime rendering when class names become known in the browser, such as in a visual editor or content loaded after the build. The runtime uses MutationObserver to track connected elements and their class changes, then inserts or reuses the matching CSS rules.

Runtime delivery

The browser engine inserts or reuses CSS rules. Cached rules can outlive their connected DOM usage.

Text description
  1. Observe. Read connected elements and receive DOM class changes.
  2. Ensure. Execute complete classes with the compiled project program.
  3. Update. Insert or reuse native CSS rules in the runtime stylesheet.

Select runtime mode through your integration, for example the Vite runtime setup. Once it is running, execute these snippets one at a time in the browser console:

  1. Insert an element

    const h1 = document.createElement('h1')h1.className = 'text-center'h1.textContent = 'Hello World'document.body.append(h1)

    The connected heading becomes centered when the runtime generates its rule.

  2. Add a class

    h1.classList.add('(font-size:3rem)')

    The heading grows to 3rem. Its existing alignment rule remains in use.

  3. Remove a class

    h1.classList.remove('text-center')

    The heading stops matching the alignment rule. It returns to its inherited alignment, even if the runtime temporarily retains the unused rule.

  4. Remove the element

    h1.remove()

    Neither class is used by this heading anymore. If no other connected element uses them, their rules can be removed during idle cleanup.

These are the rules used while both classes are present:

Generated CSS
@layer utilities {  .text-center {    text-align: center  }  .\(font-size\:3rem\) {    font-size: 3rem  }}

Runtime generation depends on JavaScript startup. Use pre-rendered CSS when the page's initial rules must already be available in the response, and runtime support only when later class names require it.

#Hide the page until the runtime starts

A runtime-only page has no rules until the runtime generates them. Add hidden to the root element so it is not painted unstyled:

<html lang="en" hidden>  <!-- Load the stylesheet and runtime using your integration's setup. --></html>

The runtime removes hidden after startup; its handled startup error path also unhides the root. If JavaScript is disabled, or the runtime script is blocked or fails to download, the page stays hidden. Use static or pre-rendered CSS for content that must remain readable then. Progressive pages already carry the first screen's CSS and stay visible.

#Preload the runtime program

The engine needs its program before it can generate the first rules. On a runtime-only page, preload the program so its request starts while the document is parsed. Match the runtime's request with as="fetch" and crossorigin, also on the same origin:

<!DOCTYPE html><html lang="en" hidden><head>  <link rel="preload" as="fetch" crossorigin href="/master-css/default-program.bin">   <link rel="stylesheet" href="/master-css/base.css">  <script src="/master-css/global.min.js"></script></head></html>
FilePreload?
CDN default-program.binYes, on CDN runtime-only pages that use the packaged default preset.
Project program master-css-program.<hash>.binYes, on runtime-only pages; official runtime integrations add the hint. No on progressive pages, where the engine and program wait until a class needs them.
Page hydration manifestNo. It is handoff data the browser does not need to paint.

Confirm in DevTools that the runtime reuses the preloaded response instead of requesting it again. See rel="preload" for the matching rules.

Preloading fetches the program; runtime startup uses it

The diagram shows a runtime-only page with a matching fetch preload. Connector lengths do not measure request duration or startup speed.

Text description
  1. A preload with as="fetch" and matching crossorigin starts the program request while the document is parsed.
  2. The hint does not execute the engine, inspect DOM classes or generate CSS. Runtime initialization consumes the matching response and performs that work.
  3. Do not add this program preload to a progressive page that intentionally defers the engine and program. The page hydration manifest is a different resource.

#Pre-rendering — SSG, SSR

Use pre-rendering when your build or server has the final page HTML and you do not need browser generation for later classes. The server renderer reads that HTML, generates its rules, and inserts them into style#master-css. It generates CSS from markup; it does not render your application components into HTML.

Official integrations perform this step for their supported HTML outputs. For a custom server or build pipeline, this example uses virtual:master-css-manifest supplied by a build integration. That virtual module must be resolved by the build tool before the server code runs.

index.html
<!doctype html><html lang="en"><head>  <meta charset="utf-8">  <meta name="viewport" content="width=device-width, initial-scale=1"></head><body>  <h1 class="font-5xl">Hello World</h1></body></html>
server.ts
import { readFileSync } from 'node:fs'import { createServerRenderer } from '@master/css-server'import manifest from 'virtual:master-css-manifest'const indexHTML = readFileSync('./index.html', 'utf-8')const renderer = createServerRenderer({ manifest, maxCachedClasses: Infinity })const { html } = renderer.renderHTML(indexHTML)renderer.dispose()

Write or serve the returned html. Its style#master-css contains the heading's rule and the font-size token it needs:

Generated CSS
@layer theme {  :root,  :host {    --font-size-5xl: 2.5rem  }}@layer utilities {  .font-5xl {    font-size: var(--font-size-5xl)  }}

No Master CSS runtime or hydration manifest is needed for this output. A class that only appears later needs CSS supplied separately, or a mode with runtime support.

The renderer can reuse cached rules across a finite static build while producing page-specific CSS. Recreate it when the manifest changes and dispose it when the build ends. For a long-lived server, choose a bounded maxCachedClasses instead of the build example's Infinity; see the server API.


#Progressive rendering

Use progressive rendering when the server or build can generate the initial page CSS and the browser may later introduce new classes. The response carries that CSS in style#master-css; the runtime reads a page hydration manifest to distinguish covered classes from those that need the engine.

Progressive delivery

Initial CSS arrives with the HTML. With a valid pre-rendered stylesheet and page manifest, the engine stays deferred until a connected class outside that manifest appears.

Text description
  1. Render. Generate CSS from the classes in the rendered page HTML.
  2. Deliver. Send the initial CSS with the HTML so the browser can apply it before runtime startup.
  3. Continue. Read the page hydration manifest and track connected DOM classes. By default, a class outside that manifest loads the browser engine and project program, adopts matching existing rules, and generates any additional CSS.

A typical output points the style element to an external manifest:

<style id="master-css" data-master-css-hydration-manifest="/_master-css/hydration/master-css-hydration.<hash>.json">...</style>

The integration supplies the actual asset URL. Custom SSR hosts that cannot publish a stable JSON asset can use inline script#master-css-hydration-manifest instead. Pages without generated rules or managed resources do not need a manifest pointer.

The browser can apply the inline CSS before the runtime or manifest loads. This describes when CSS is available; actual paint timing still depends on the page's other resources. Do not preload the page hydration manifest or the deferred project program; see preloading.

A class listed in the manifest can be reused without starting the engine. This includes ordinary authored classes and rule-free state or theme classes referenced by generated or project selectors. Changing an attribute used by an already-rendered selector also needs no new class rule. A complete class absent from the manifest starts the engine, even if it appeared elsewhere in the project's source.

A known class reuses delivered CSS

This is the default progressive handoff with valid pre-rendered artifacts. It is not a performance timeline.

Text description

A browser has CSS and a valid page manifest. A known connected class follows the existing CSS path; a class outside the manifest branches to deferred engine loading.

#CSS hydration

Hydration lets the runtime adopt existing CSS rules. At startup it reads the explicit hydrationManifest option, or discovers the inline manifest or the URL on style#master-css. With the engine deferred, it tracks usage without rebuilding the stylesheet.

CSS hydration

When the engine starts, matching rules are adopted; a mismatch rebuilds CSS. A referenced manifest that cannot load or validate rejects startup before this comparison.

Text description
  1. Read. Load the page hydration manifest; keep using the existing stylesheet.
  2. Validate. When the browser engine starts, use the project program to replay manifest classes in server rule order, validate resources, and compare the generated rules with the existing CSSOM.
  3. Adopt or rebuild. Reuse matching rules as runtime-managed rule objects. A mismatch rebuilds CSS.

When a new class or cssRuntime.ready() starts the engine, the browser engine replays the manifest's classes in server rule order, validates resources, and compares the generated result with the existing CSSOM. Matching rules become runtime-managed rules; new classes can then generate additional CSS. Removed classes may keep their pre-rendered rules until this handoff, then enter the normal retained-rule cleanup path.

The manifest carries resource order and the globals already emitted by the page's stylesheets. It does not repeat the CSS text or replace the project program. Keep it with the HTML and assets from the same build.


#Integration support

Static rendering is the default for these official adapters. Runtime generation is an explicit choice for classes that are unknown at build time. pre-render generates CSS from HTML without a client runtime; progressive combines HTML rendering with a runtime for later classes. The HTML each adapter can process depends on its host:

AdapterStaticRuntimepre-render and server HTMLprogressive
ViteBuild-time extractionInjected through Vite's HTML pipelineHTML processed by Vite hooks; custom SSR needs a server integrationVite HTML rendering plus runtime; custom SSR needs its own HTML rendering
WebpackBuild-time extractionInjected into compilation HTML assetsYour HTML host must call @master/css-server; the plugin has no request-time server hookRuntime delivery is provided; your host supplies rendered HTML and hydration data
Next.jsBuild-time extractionClient instrumentationHTML outputs processed after the build; dynamic request-time SSR is not transformedBuild-rendered HTML plus runtime; dynamic SSR has no automatic server CSS injection
AstroBuild-time extraction through ViteInjected page scriptAstro middleware processes prerendered pages and server HTML responsesMiddleware rendering plus runtime
NuxtBuild-time extraction through ViteNuxt client pluginNitro HTML responses with string bodies; streaming bodies are not transformedNitro HTML rendering plus client runtime
SvelteKitBuild-time extraction through ViteRequires the documented client hookRequires the Master CSS server handle; it processes page chunks during SSR and prerenderingRequires both the server handle and client hook

Vite and Webpack cannot add server behavior to a framework merely because it uses that bundler. Webpack's HTML injection also does not rewrite files served directly from a development server's static directory. Astro middleware reads the complete HTML response before returning it; SvelteKit uses its page-chunk hook. Follow the framework guide when choosing the delivery path. PHP, Rails, CMS and .NET hosts build the asset with Vite and load it from their templates; see Server-rendered apps.

#Native pruning is a separate choice

Progressive rendering can use native pruning without changing when initial CSS is delivered or later utilities are generated. Pruning requires a known usage set and a build path that supplies it; runtime generation cannot restore an arbitrary native selector removed from delivery. See Optimizing CSS delivery for a reproducible comparison.

#Verify the selected mode

Test a production build as well as development: an integration may use runtime generation during development even when production pre-renders CSS.

ModeWhat to verify
StaticThe generated HTML loads the stylesheet, and representative complete source classes have rules in it. Those styles remain with JavaScript disabled.
Pre-renderThe response source contains style#master-css with the page's rules. Verify before running JavaScript; the live DOM alone cannot prove pre-rendering occurred.
RuntimeInsert a class absent from the initial DOM and check its computed style and generated CSS rule after the runtime observes it.
ProgressiveVerify pre-rendered CSS first, then introduce a class absent from the page manifest and check that its rule is generated. Test dynamic SSR routes separately if the adapter only processes build-time HTML.

#Inspect the running stylesheet

The runtime owns style#master-css and updates its CSSOM. The element's text content is not a reliable live view after startup. Inspect the stylesheet's cssRules instead:

const sheet = document.querySelector('style#master-css').sheetconsole.log(sheet.cssRules)

#Diagnose progressive startup

ObservationMeaning and next check
CSS is rebuilt after a warningThe existing rules do not match the hydration data, or the style has no manifest pointer. Check that HTML, CSS and manifest assets belong to the same build.
Startup rejects with INVALID_HYDRATION_MANIFESTA referenced inline or external manifest could not be loaded or validated. Check the asset URL, response and deployed files.
Initial CSS works but new classes do notThe deferred engine may have failed to load. Inspect onDiagnostic, the program request and cssRuntime.snapshot().engine. Existing pre-rendered CSS is retained; refresh() or a new start() can retry.

When new classes are likely soon after load, enginePrefetch: 'idle' starts the engine during idle time. Use engine: 'eager' to start it during start(). Both change the default deferral tradeoff and should be chosen for the page's actual interactions.

#Interaction-heavy screens

An interaction that adds many first-time classes asks the browser to process both DOM changes and new stylesheet rules. For editors, data grids, builders, or other large dynamic views:

  • Keep stable layout, typography and component states in shared classes.
  • Warm predictable runtime classes before inserting the view, after the engine is ready.
  • Use CSS variables or inline styles for volatile per-element values such as coordinates or user-generated colors.
  • Use native selectors for repeated states that do not need a distinct generated class for every value.

To warm the predictable classes for a view, use the handle returned by MasterCSSRuntime.start():

await cssRuntime.ready()cssRuntime.ensureClassRules(['grid', 'gap-md'])

Measure runtime JavaScript, program delivery, initialization, DOM observation, and CSSOM work together. Compressed JavaScript size alone does not describe interaction cost. See Benchmarks for measurements and their conditions.

#User-generated markup

Every class the runtime observes or the server renders becomes CSS, so a class attribute is as powerful as a style attribute: native declarations, url() and selectors such as :has() included. When pages render markup other people write, such as comments, CMS rich text or chat messages, treat class in your HTML sanitizer the way you treat style: remove it, or allow only classes you choose. Escaping the attribute value is not enough, because the class name itself is the input.

For runtime state, delivery artifacts, custom startup entries and engine hosts, see the runtime, server and engine packages.