refract v0.1 · beta
Delivery / Build-time

Build-time

Compile once in your build or CI and ship static files — plain CSS, tokens, SCSS. Zero runtime: the browser just loads the stylesheet, and refract never reaches the client bundle.

The CLI reads a theme.config.ts, runs each target's adapter, and writes the emitted files to disk. This layer uses Node (fs, TypeScript) — it runs at build, not in the browser.

theme.config.ts
import { defineConfig } from "@theme-registry/refract/build";
import { createCssAdapter } from "@theme-registry/refract-css";
import { createScssAdapter } from "@theme-registry/refract-scss";
import { raw } from "./theme.raw";

export default defineConfig({
  raw,
  targets: [
    { name: "css",  adapter: createCssAdapter(),  outDir: "dist/theme" },
    { name: "scss", adapter: createScssAdapter(), outDir: "dist/scss" },
  ],
});

defineConfig(config) — config reference

PropertyTypeRequiredDefaultDescription
rawRawThemeRequiredThe theme you author — the single source compiled for every target.
targetsEmitTarget[]RequiredOne or more output targets. A single theme can emit CSS + SCSS + styled-components at once.
mediaMediaConfigOptional{ unit: "px" }Media-query output unit for @media + @container widths — { unit: "px" | "em" | "rem"; baseFontSize?: number } (thresholds are authored in px; baseFontSize defaults to 16).
unitsUnitsConfigOptionalpx seedLength units for declaration values (§21) — the build-time twin of createTheme's units, so refract build emits the same unit to disk. A token-path role map (units.default, units["<subsystem>"], units["<subsystem>.<property>"]; most-specific wins over the built-in px seed). Resolved once, format-neutrally, so every target emits the same unit. See Length units.
baseFontSizenumberOptional16Divisor when a deferred length resolves to rem (§21). Distinct from media.baseFontSize.

EmitTarget — target reference

PropertyTypeRequiredDefaultDescription
adapterThemeAdapterRequiredThe constructed adapter that renders this target — e.g. createCssAdapter(). The import is the extensibility seam; pass adapter options at construction.
outDirstringRequiredDirectory the emitted files are written to, relative to the config.
namestringOptionalindex / outDirA label for the target; lets refract build --target <name> select just this one.
emitEmitOptional"single"Output-shape directive — how the files are written (single / split / subsystem / components). See the emit API below.
helpersstring[]Optional[]Vendored, self-contained runtime helper modules to also emit alongside the CSS. Currently "color-math" — the lighten/darken module for live colour math in the browser.
guideboolean | GuideConfigOptionalfalseEmit a self-documenting llms.txt + manifest.json into outDir so the shipped theme carries its own AI consumption guide — real class names / export ids / token paths — for a downstream dev who has neither refract nor its skills. true uses defaults; an object tunes file names or adds packageName for a by-specifier import overlay. See Self-documenting output.
previewboolean | PreviewConfigOptionalfalseEmit a human-facing preview.html specimen into outDir — token plates for any adapter, plus live recipe plates when the output is browser-loadable (CSS). true uses defaults; an object tunes { file, title, inline }. See Human-facing preview.
Terminal
refract create              # design a theme.raw.(ts|js|json) from one seed colour
refract init                # scaffold theme.config.ts (imports theme.raw.* if present)
refract import tokens.json   # seed theme.raw.ts (+ config) from a DTCG tokens.json
refract build               # compile every target → write files
refract tokens --out tokens.json   # DTCG export (adapter-free)
refract audit --strict      # score colour contrast (WCAG + APCA); fail on a miss
refract skills install      # install the AI skills into your agent (claude/codex/…)
dist/theme/theme.css · emitted
:root {
  --dt-colors-brand: #4c6ef5;
  --dt-colors-brand-dark: #334dd2;
}
.dt-colors-solid-brand {
  background: var(--dt-colors-brand);
  color: var(--dt-colors-brand-text);
}
emit modes: each target's emit picks how it's written — one file, split (styles + variables), per-subsystem, or self-contained per-components. Ship the precompiled output; nothing from refract runs in production.

The emit API — output modes

Set emit on any target. Four modes; every field is optional and defaults are filled by the build layer. The CSS adapter is the reference realization — other adapters honor a subset and throw a clear error on a mode they don't map.

ModetypeWritesDescription
single"single" · defaulttheme.cssOne file — every subsystem's :root variables plus all rules. The global adapter inline option bakes resolved values whole-file.
split"split"styles.css · variables.cssTwo files: all rules + all :root variables. Load-order contract — variables must load first, no @import; global inline is rejected.
subsystem{ type: "subsystem" }<sub>.css · <sub>.variables.css × NA styles + variables pair per subsystem. Same load-order contract; components owns no properties → emits styles only.
components{ type: "components" }${group}-${variant}.css (+ variables.css)The only mode that flattens each variant into one self-contained rule-set — referenced recipes' declarations + own css delta (delta wins).

Options per mode

ModeOptionTypeRequiredDefaultDescription
singlefilestringOptional"theme.css"The output filename.
splitfilestringOptional"styles.css"The rules file.
splitvariablesstringOptional"variables.css"The :root vars file. Its mere presence infers type: "split".
subsystemfilename(subsystem, kind) => stringOptional<sub>.css / <sub>.variables.cssNames both files of each pair. kind is "styles" | "variables".
componentsinlinebooleanOptionaltrueBake literal values (zero var(, fully portable). falsevar(--…) refs plus a tree-shaken variables file.
componentsfilename(c) => stringOptional`${group}-${variant}.css`File assignment. Variants returning the same name are concatenated — () => "components.css" collapses all into one.
componentsvariablesstring | falseOptional"variables.css"The tree-shaken vars filename (used when inline: false); false suppresses it (the consumer supplies the vars).
discriminator: only single / split may omit type — a bare { variables } infers split; subsystem and components always need an explicit type (or the string shorthand). The refract skills subcommand (also on this CLI) is documented under AI skills.

Self-documenting output — guide

Set guide: true on any target and refract writes two extra files into that target's outDir: an llms.txt narrative and a manifest.json index. They name the theme's real identities — the emitted class names / export ids / dotted token paths — so a developer (or their coding agent) can consume the theme from the folder alone, with no refract, no skills, and no guessing at names. The files travel inside outDir, so they accompany the theme however it ships: an npm package, a CI zip, or a vendored folder.

theme.config.ts
export default defineConfig({
  raw,
  targets: [
    {
      name: "css",
      adapter: createCssAdapter(),
      outDir: "dist/theme",
      guide: { packageName: "@acme/theme" },  // or just `guide: true`
    },
  ],
});
dist/theme/llms.txt · emitted
# Theme consumption guide (css)

This folder is a self-contained theme built with refract. You do NOT need refract to use it …

## How to use
Import the stylesheet once, then apply the recipe class names below to your elements.
Example: `import "./theme.css";` then `<button class="<class>">`.

If this theme is installed as the `@acme/theme` package, you may import the same
files by specifier (e.g. `@acme/theme/theme.css`) instead of by relative path.

## Recipes — real names
| Recipe                      | Identity                                          |
| --------------------------- | ------------------------------------------------- |
| `colors.solid.brand`        | `dt-colors-solid-brand`                           |
| `components.buttons.primary`| `dt-colors-solid-brand dt-components-buttons-primary` |
The prose is adapter-specialized — the styled-components target names ThemeProvider + the tree-shakeable css export ids, SCSS names the @use partials + class names, JSON names the dotted ruleSets keys. manifest.json pairs the recipe index with the theme's full token set (DTCG), and carries a schema version (currently 1) so an agent can bind to a known contract shape. Off by default — a build with no guide is byte-identical.

Human-facing preview — preview

guide writes for an agent; preview writes for a person. Set preview: true on any target and refract writes a preview.html into that target's outDir — a rendered specimen of the theme you double-click, forward, or hand a designer. By default it inlines its stylesheets, so the page is a single self-contained file that keeps working after it's moved out of outDir.

theme.config.ts
export default defineConfig({
  raw,
  targets: [
    {
      name: "css",
      adapter: createCssAdapter(),
      outDir: "dist/theme",
      preview: true,  // or { file, title, inline: false }
    },
  ],
});

It reads as a proper style guide rather than a token dump. The page is a light paper and each plate a card: a masthead in your theme's own first palette with headline counts (including a WCAG pass ratio across declared pairings), an index whose entries are the shape of the theme, one card per colour family with a large base swatch and its lightness ladder, the type ramp set in its own sizes, spacing shown as a measure and as an applied inset (there is no separate padding token — spacing is the padding scale), a state matrix per recipe, and a copy-on-click identifier beside every specimen. Sections appear only when the theme has tokens of that kind.

Two things follow from what refract knows. Each value is tagged src or gen — whether you authored it or refract synthesised it — read from the model, since the token export resolves both to plain literals. And the sheet is deliberately light-only: colour can't be judged against a moving backdrop, and a flipping page would make it impossible to tell whether a swatch changed because the theme's mode changed or because the page did. The appearance control moves the specimen, not the sheet.

The page has two halves, and they degrade independently:

HalfSourceWorks with
Token platesThe format-neutral token export — colours, the type ramp, spacing, radii, shadows, borders, breakpoints, each with its exact value.Every adapter, including third-party ones. Nothing about the emitted format is involved.
Recipe platesThe emitted stylesheet, loaded into the page, with each recipe rendered on its real class list.Adapters whose output a browser can load as-is — CSS today.

For a target whose output isn't browser-loadable, the page still renders every token and still lists every recipe by its real identity — it just says, in that adapter's own words, why it can't render them live. SCSS needs compiling first; styled-components emits JS modules that need React; JSON is data with no rendered form. For a live design-review page from a styled-components or SCSS theme, add a CSS target to the same config — it compiles the same recipes through the same core, so it's a faithful specimen.

Three things only a compiler's specimen sheet can show. States render side by side rather than on hover — a CSS pseudo-class can't be triggered from markup, so the adapter emits a parallel pinnable rule that is inlined into the page and never added to the stylesheet you ship. Appearance modes get a diff table of the tokens that actually carry an override, so you can see the cause and not just the result. Composition is broken into its parts, attributing each class in a component's identity to the recipe it came from. Bare elements themed by the globals subsystem get their own prose specimen, since they carry no class at all.
The preview follows your emit mode. It references the files that were actually written — split loads the variables file before the styles file, subsystem loads every pair and groups the plates by subsystem, and components groups them by the file each component landed in and marks the recipes that mode doesn't emit. A theme with appearance modes gets a light/dark toggle; one with breakpoints gets frame-width buttons. Off by default — a build with no preview is byte-identical.
No recipes yet? A freshly scaffolded theme (see Scaffold a theme) has tokens and no recipes, so the preview renders your palette and ramps and then tells you exactly what's missing — it's the fastest way to see what you got before writing your first recipe.
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.