refract v0.1 · beta
Adapters / CSS

CSS adapter createCssAdapter

Renders a Theme to a plain-CSS stylesheet — :root custom-property blocks plus the recipe class rules that reference them — and adds a class surface for using them in markup. The full surface it adds to the theme is below.

theme.ts
import { createTheme } from "@theme-registry/refract";
import { createCssAdapter } from "@theme-registry/refract-css";

const theme = createTheme(raw, { adapter: createCssAdapter({ prefix: "acme" }) });
theme.css; // the full stylesheet

Every option is optional. Two prefix knobs — one for variables, one for classes — cover the common case (for full control there's the naming override), plus value/delivery knobs. Subsystems are namespaced by the token path, so by default a variable is --<prefix>-<subsystem>-<group>-<variant> and a recipe class is .<classPrefix>-<subsystem>-<group>-<variant>.

The theme surface

On top of the base theme (model · tokens · resolveToken · override, from core), the CSS adapter adds:

MemberTypeWhat it is
cssstringThe full stylesheet — :root variable blocks + every recipe class rule.
variablesCssstringJust the :root custom-property blocks.
recipesCssstringJust the recipe class rules.
classesRecord<sub, Record<group, Record<variant, …>>>The class map — subsystem → group → variant. A component variant is { className, classList }; every other subsystem's recipe is a class-name string.
getClass(sub, group, variant) => string | undefinedThe ergonomic accessor over classes: the class string for one address (the composed className for components), or undefined if the address doesn't exist.
varName(path) => string | undefinedA token path's emitted CSS custom-property name — varName("colors.brand.dark")--dt-colors-brand-dark (carrying the configured prefix), or undefined for a path that mints no variable. The var-name twin of resolveToken's value.
renderRecipe(sub, group, variant) => stringOne recipe's own CSS — base declarations + its state / breakpoint rules. The finer-grained sibling of css.
nodesCssNode[]The structured CSS-node IR behind css — for custom rendering or tooling.
mediaMediaDescriptorBreakpoint → @media builder (the same descriptor used to lower responsive rules).
app.ts
theme.getClass("components", "buttons", "primary");
// → "dt-colors-solid-brand … dt-components-buttons-primary"
theme.getClass("colors", "solid", "brand");   // → "dt-colors-solid-brand"
theme.getClass("colors", "nope", "x");      // → undefined

Helper functions

Beyond the stylesheet, the adapter hands you helpers for the two places you write CSS outside a recipe — at run time, and after a build-time emit.

Run-time — theme.media

A media-query builder over your named breakpoints, so custom CSS (or a styled block) stays in sync with the theme instead of hard-coding pixel widths. It returns the @media string; max / between use the next breakpoint's edge automatically.

media.ts
theme.media.min("md");            // "@media (min-width: 768px)"
theme.media.max("md");            // "@media (max-width: 1023.98px)"
theme.media.between("sm", "lg");  // "@media (min-width: 576px) and (max-width: 1023.98px)"

// per-breakpoint group — precomputed strings:
theme.media.md.min;             // "@media (min-width: 768px)"
The emitted media unit (px / em / rem) follows the media config, so these strings match what your recipes' responsive rules emit. (The styled-components adapter decorates theme.media as tagged templates for styled blocks.)

Build-time — vendored color-math

When you emit static CSS and drop refract from the runtime, you sometimes still need to compute a colour live (a user-picked accent, say). Opt a build target into the color-math helper and refract writes a standalone color-math.js next to your CSS — the exact functions it used to synthesize your palette, so a value computed in the browser matches the emitted variables.

theme.config.ts
{ adapter: createCssAdapter(), outDir: "./theme", helpers: ["color-math"] }

// → ./theme/color-math.js  (no refract dependency)
import { lighten, darken, alpha } from "./theme/color-math.js";
lighten("#4c6ef5", 14);   // same maths as the emitted --dt-colors-…-light
HelperExportsWhy
color-mathlighten · darken · alpha · setL · rotateHue · complement · adjust · rgbToOklch · oklchToRgb · toHexColor · toOklchColor · convertHexToRGB · convertRgbToHexThe pure colour maths refract synthesizes steps with — so live values match the emitted CSS. Self-contained (zero refract import). The same functions (plus parseColor/serializeColor/isHexColor) are importable directly from the @theme-registry/refract/color-math subpath when you keep refract installed.
Opt in per build target via helpers: [ … ]. Adapters can also ship theme-specific vendored helpers automatically (the SC adapter emits a baked media module); the CSS adapter needs none, so its only vendored helper is the opt-in color-math.

Options

OptionTypeDefaultAffects
prefixstring"dt"Every variable name (--<prefix>-…); also the default for classPrefix.
classPrefixstring= prefixEvery class name — recipe classes and container-query context classes.
inlinebooleanfalseBakes resolved values into rules and drops the :root variable blocks.
colorFormat"rgb" | "hex" | "oklch""rgb"Output syntax for palette colour variables (see below).
naming{ className?, variableName? }Full class / variable name control beyond the prefix — two formatters over the structured address. See Naming overrides.
layerstring | booleanWrap all output in a named cascade @layer (truerefract) for deterministic precedence below unlayered app CSS. Single-file emit only; off = byte-identical. Also on the SCSS adapter.
reducedMotionbooleanfalseAppend a @media (prefers-reduced-motion: reduce) block that neutralizes animation + transition durations.

colorFormat — palette colour output

Choose how palette colour values are written into the :root variables — base, text, variants, responsive overrides, and appearance modes. The colour is identical across formats; this is presentation only. The Model always stores the canonical rgb(); hex / oklch re-serialize that same colour at emit time. oklch unlocks the browser's native OKLCH interpolation for anything reading these variables.

colorFormat.ts
createCssAdapter();                         // --dt-colors-primary: rgb(77, 171, 247);
createCssAdapter({ colorFormat: "hex" });   // --dt-colors-primary: #4dabf7;  (#rrggbbaa with alpha)
createCssAdapter({ colorFormat: "oklch" }); // --dt-colors-primary: oklch(71.8% 0.1422 246.06);
The oklch() reflects the 8-bit-quantized value (the rgb() is the canonical colour). Colours typed literally into a recipe pass through as authored — colorFormat applies to synthesized palette tokens; inline mode inherits the format. Other adapters (SCSS / JSON / styled-components) and DTCG export are unaffected.

prefix — variable names (and the MFE story)

prefix sets the identifier segment on every CSS variable, and is the default for classPrefix, so on its own it rebrands the whole output:

createCssAdapter({ prefix: "acme" })
:root { --acme-layout-spacing: 16px; }
.acme-layout-container-full { gap: var(--acme-layout-spacing); }

Micro-frontend isolation. Two independently-built bundles on one page must not share variable names, or their --* custom properties collide. Give each build a distinct prefix:

two bundles, one page
// App A
createCssAdapter();                       // → --dt-*        / .dt-*
// App B
createCssAdapter({ prefix: "checkout" }); // → --checkout-*  / .checkout-*

Because a variable's first segment is the prefix by construction, choosing the prefix is the whole isolation mechanism — there's no separate “scope” concept.

classPrefix — class names

By default classes inherit prefix. Set classPrefix only when you want the class names to differ from the variable names:

createCssAdapter({ prefix: "acme", classPrefix: "ui" })
:root { --acme-layout-spacing: 16px; }        /* variables → prefix */
.ui-layout-container-full { gap: var(--acme-layout-spacing); }  /* classes → classPrefix */

This one prefix covers every class the adapter emits, including the container-query context utility classes — there is no per-family override:

container context class
.ui-cq-card { container-type: inline-size; container-name: card; }

inline — bake values, drop variables

By default the adapter emits var(--…) references plus the :root blocks that define them. inline: true resolves each reference to its concrete value, bakes it into the declaration, and omits the variable blocks entirely.

default vs inline: true
/* default */
:root { --dt-colors-primary: #4dabf7; }
.dt-colors-solid-primary { background: var(--dt-colors-primary); }

/* inline: true */
.dt-colors-solid-primary { background: #4dabf7; }
Use the default for runtime theming (swap a :root var and everything updates); use inline for a self-contained stylesheet with no custom-property indirection. It leaves no variables file, so it can't be combined with the multi-file emit modes that produce a separate variables file (the adapter throws). Note the components emit mode has its own inline control (default true) on the emit plan; this global inline doesn't drive it.

Value units

The length unit for declaration values is not a CSS-adapter option — it's resolved format-neutrally in core from createTheme's units config and baked onto the Model, so every adapter emits the same unit. See Length units.

Technical documentation for @theme-registry/refract · all output is compiled client-side by the real library (CSS adapter). The chrome is theme-aware; the render panes carry each preset's own world. · MIT licensed.