refract v0.1 · beta
Adapters / styled-components

styled-components adapter createStyledComponentsAdapter

Emits TypeScript/JavaScript modules, not a stylesheet — a literal theme object and tree-shakeable css recipes whose values read straight from the theme (no var()), a base-layer GlobalStyle (from the globals subsystem), and the types that make props.theme typed. Runtime surfaces: theme.theme · recipes · GlobalStyle · media · scheme.

OptionTypeDefaultDescription
language"ts" | "js""ts"ts also emits theme.d.ts (augments DefaultTheme).
scheme"media" | "attribute" | "both""media"How appearance modes realize dark: prefers-color-scheme, a [data-theme] toggle, or both.
helpersstring[][]["color-math"] wires the lighten/darken/alpha import and hangs them on theme.
emit (target)"single" | "split""single"One module, or theme.ts / recipes.ts / global.ts split.
prefixstringAccepted for parity with the CSS adapter's namer. SC identifiers carry no prefix (the literal theme object is the isolation boundary), so in v1 this does not alter emitted identifiers.
classPrefixstringAs prefix — accepted for parity, structural in v1.
namingNamingOverridesShared naming-override type with the CSS adapter. className remaps a recipe's export identity (camelCased); variableName has no coherent target on the nested theme object and is accepted-but-structural (keys stay derived from the token address).

The object is the indirection — every token a resolved literal, variants folded into their group as camelCase keys (primaryprimaryText). Dark mode lives inside each recipe (a theme.scheme.dark / [data-theme] block), so it tree-shakes with the recipe and switches with a plain CSS recalc — one ThemeProvider, never swapped.

theme.ts (emitted)
export const theme = {
  colors: { primary: "rgb(20, 126, 255)", primaryText: "rgb(255, 255, 255)" },
  layout: { spacingMd: "16px", spacingLg: "24px" },   // variants fold into their group as camelCase
  media, scheme,
} as const;

// flat, tree-shakeable — values read from the theme, no var()
export const colorsSolidPrimary = css`
  background: ${({ theme }) => theme.colors.primary};
  color: ${({ theme }) => theme.colors.primaryText};
`;
// composition = a css spread of the referenced siblings + own delta
export const componentsButtonsPrimary = css`
  ${colorsSolidPrimary} ${typographyControlButton} ${layoutPaddingControlMedium}
  cursor: pointer; border: none; border-radius: 26px;
`;

Conditional blocks — states, responsive & dark

A recipe is one css block that carries its own states, breakpoints and appearance modes as nested blocks — every value still read from the theme. Breakpoints come off theme.media.<bp>.{min,max,exact,between}; dark comes off theme.scheme.dark, reading the mode's values from theme.modes.dark.*. Because dark lives inside the recipe, it tree-shakes with it and switches as a pure CSS recalc — no global :root, no provider swap, no flash.

recipe with all three (emitted)
export const colorsSolidPrimary = css`
  background: ${({ theme }) => theme.colors.primary};
  color: ${({ theme }) => theme.colors.primaryText};
  ${({ theme }) => theme.media.md.min`      /* responsive → @media (min-width: 768px) */
    background: ${theme.colors.primaryDark};
  `}
  &:hover {                                /* state */
    background: ${({ theme }) => theme.colors.primaryDark};
  }
  ${({ theme }) => theme.scheme.dark`         /* dark — reads theme.modes.dark.* */
    background: ${theme.modes.dark.colors.primary};
  `}
`;

The scheme option decides how the dark block is realized. "media" (default) wraps it in @media (prefers-color-scheme: dark) — follows the OS. "attribute" emits a [data-theme="dark"] & selector instead — a manual toggle wins. "both" emits both.

scheme: "attribute" (emitted)
  [data-theme="dark"] & {
    background: ${({ theme }) => theme.modes.dark.colors.primary};
  }

Consuming the emitted modules — build-time

Run the adapter at build time (refract build or emit()) to write the modules, then import them. The theme object drives one ThemeProvider, set once; recipes drop straight into a styled block; refract itself never reaches the bundle. Ad-hoc styling reads literals off props.theme — and because those are real values, the opted-in color-math helpers run on them.

App.tsx
import styled, { ThemeProvider } from "styled-components";
import { theme, GlobalStyle, componentsButtonsPrimary } from "./theme";

const Button = styled.button`${componentsButtonsPrimary}`;

// provider set once — dark mode is @media, no swap
<ThemeProvider theme={theme}>
  <GlobalStyle />
  <Button>Book flight</Button>
</ThemeProvider>

Runtime — the same shapes, live

createTheme(raw, { adapter }) returns the same surface the emit writes — theme · recipes · GlobalStyle · media · scheme — but each recipe is lowered to a live css block on first access and cached (nothing pre-generated). No build step, and override() yields a fresh live surface — ideal for dynamic or white-label themes.

runtime.tsx
import styled, { ThemeProvider } from "styled-components";
import { createTheme } from "@theme-registry/refract";
import { createStyledComponentsAdapter } from "@theme-registry/refract-styled-components";

const t = createTheme(raw, { adapter: createStyledComponentsAdapter() });
const Button = styled.button`${t.recipes.components.buttons.primary}`;

<ThemeProvider theme={t.theme}>
  {t.GlobalStyle && <t.GlobalStyle />}
  <Button>Book flight</Button>
</ThemeProvider>

// white-label: override() → a new live surface, no rebuild
const brandB = t.override({ colors: { primary: { base: "#e8590c" } } });
Build-time — emit() / refract buildRuntime — createTheme
Formserialized .ts/.js moduleslive css RuleSets, lazy + cached
refract at runtimegone — zero dependencypresent
Best forship, tree-shake, drop refractdynamic / white-label, override(), no build step
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.