refract v0.1 · beta
Guides / React

React

refract is framework-agnostic — there's no React binding to install. With the CSS adapter you inject theme.css once and apply theme.classes.*.className. Here's the idiomatic wiring: a provider, class usage, and switching override() children in state.

1 · Load the stylesheet & provide the theme

theme.css is the entire stylesheet as a string — the :root custom properties plus every recipe class. Load it the way you'd load any CSS: inject a <style> tag at run-time (below), or emit it to a .css file at build time and <link> it (then you can drop refract from the client bundle). The provider does the run-time injection once and exposes the theme via context so components can read their classes.

theme-provider.tsx
import { createContext, useContext } from "react";
import type { Theme } from "@theme-registry/refract";

const ThemeContext = createContext<Theme | null>(null);
export const useTheme = () => useContext(ThemeContext)!;

export function ThemeProvider({ theme, children }) {
  return (
    <ThemeContext.Provider value={theme}>
      {/* inject the stylesheet once, at the root */}
      <style dangerouslySetInnerHTML={{ __html: theme.css }} />
      {children}
    </ThemeContext.Provider>
  );
}
Content-Security-Policy. Injecting a <style> at run-time needs your style-src to permit it — either add a per-request nonce (<style nonce={nonce}>…</style> matching a style-src 'nonce-…' header) or, under a strict policy, prefer build-time delivery: emit theme.css to a file and <link> it (see Build-time) — a static stylesheet needs no style-src exception and drops refract from the client bundle entirely. The multi-tenant, per-request-theme case is exactly where the nonce matters; wire the theme's CSS through the same nonce your framework already issues.

2 · Use a class

The quickest way to a class is theme.getClass(subsystem, group, variant) — it returns the ready-to-apply string (the fully composed list for a components variant), or undefined for an unknown address. refract has already built the composition; you're just reading it.

save-button.tsx
function SaveButton() {
  const theme = useTheme();
  return <button className={theme.getClass("components", "buttons", "primary")}>Save</button>;
}
// → <button class="dt-colors-solid-brand … dt-components-buttons-primary">

Prefer the data form? theme.classes.components.buttons.primary is a { className, classList } pair — classList (the array) pairs well with a clsx-style helper for conditional classes. Recipes from the other subsystems are a plain class-name string.

Need a single recipe's CSS rather than its class? That's theme.renderRecipe(subsystem, group, variant) — the finer-grained sibling of theme.css. See the full CSS adapter surface.
Adapter-scoped. theme.getClass / theme.classes / theme.css are the CSS adapter's theme surface — the class-first model this guide (and the Angular/Vue guides) use. With the styled-components adapter you don't apply class names at all: you import its emitted recipes and render styled components, so there's no getClass. Pick one adapter per app.

3 · Switch themes at run-time

Keep the active theme in state and swap between the base and an override() child. Because the provider injects theme.css, changing the state value re-themes the whole tree — the child is a real, parent-untouched theme.

app.tsx
import { useState } from "react";
import { createTheme } from "@theme-registry/refract";
import { createCssAdapter } from "@theme-registry/refract-css";

const base = createTheme(raw, { adapter: createCssAdapter() });
const warm = base.override({ colors: { brand: { base: "#e8590c", text: "#ffffff" } } });

export function App() {
  const [theme, setTheme] = useState(base);
  return (
    <ThemeProvider theme={theme}>
      <button onClick={() => setTheme(theme === base ? warm : base)}>Swap brand</button>
      <SaveButton />
    </ThemeProvider>
  );
}
For dark mode, don't swap themes — author modes and toggle data-theme; it's a CSS-cascade flip with no re-render. Reserve theme-swapping for genuinely different themes (brands, presets).

Inline styles?

Prefer classes. Inline style={{…}} can't express :hover/:disabled states, @media responsive, container queries, or @keyframes — which is most of what a recipe emits — so a class carries far more than an inline value can. The one fair use is a one-off dynamic value; even then:

inline.tsx
// ✅ a var() reference still flips with the theme / dark-mode cascade
<div style={{ color: "var(--dt-colors-brand)" }} />

// ⚠️ a resolved literal is frozen — it will NOT re-theme
<div style={{ color: theme.resolveToken("colors.brand") }} />

Use resolveToken for logic (canvas, measurement, a computed value) — not for styling that should stay themable. The var(--…) name follows the naming grammar.

Using styled-components instead

If your app is on styled-components, use the styled-components adapter — it emits a literal theme object and tree-shakeable css recipes instead of a stylesheet. Wrap the app in one <ThemeProvider theme={theme}> and drop recipes into styled blocks (styled.button`${componentsButtonsPrimary}`); dark mode is an @media block inside each recipe, no provider swap.

Server-rendering with Next.js — per-request themes and avoiding a flash — has its own Next.js / SSR guide. The provider above already works inside a client component.
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.