Library

Themes

Sixteen first-party presets ship with Clay, eleven palette themes plus five showcase themes that retune geometry, borders, typography, motion, and translucency.

01Gallery16 presets

Each card renders under its own theme, a side-by-side visual diff. Click a card to apply that theme site-wide. Clay itself never reads or writes localStorage, see Theming → Persist for how to persist the choice in your app. The five showcase themes, Brutalist, Editorial, Terminal, Skeuomorph, Glass, go beyond colour: zero-radius mono brutalism, frosted translucent layers, full monospace, heavy shadows, the works.

02Apply

Theme the whole app

applyTheme(theme) injects a <style id="clay-theme"> tag into the head with both :root and dark-mode blocks. Toggling data-mode=“dark” on <html> flips between them via CSS, no re-render, no JS re-run.

tsxglobally.tsx9 lines
import { applyTheme, nord } from "@brika/clay/themes";

const cleanup = applyTheme(nord);

// Toggle dark mode without re-applying:
document.documentElement.dataset.mode = "dark";

// Or restore the stylesheet defaults:
cleanup();
03Scope

Or scope it to a subtree

Three idioms, each one sharper than the last. Default wraps in a display: contents div (no layout box). asChild merges the scope onto an existing child via Radix Slot (no extra DOM at all). Manual spreads the raw themeToCssVars(…) object onto any element you control.

tsxscoped.tsx6 lines
import { ocean, ThemeScope } from "@brika/clay/themes";

<ThemeScope theme={ocean} mode="light">
  <Button>Ocean button</Button>
  <Card>Ocean card</Card>
</ThemeScope>
tsxscoped-aschild.tsx7 lines
import { ocean, ThemeScope } from "@brika/clay/themes";

<ThemeScope theme={ocean} asChild>
  <article className="prose">
    Theme variables merge onto this article, no extra wrapper at all.
  </article>
</ThemeScope>
tsxscoped-manual.tsx5 lines
import { ocean, themeToCssVars } from "@brika/clay/themes";

<div style={themeToCssVars(ocean, "light")}>
  <Button>Ocean button</Button>
</div>

All three return a complete CSS-variable map (registry defaults + theme overrides), so a globally-applied theme can never bleed into the scope through a token your theme didn’t override. This is why the gallery above can show 16 cards side-by-side, each isolated.

04Shape

ThemeConfig

Every preset is plain JSON. The minimum is the four identity fields; every section below them is optional, so a colour-only theme stays small while a showcase theme can retune geometry, borders, motion, focus, and per-component tokens.

tstypes.ts18 lines
interface ThemeConfig {
id: string;
name: string;
description: string;
accentSwatches: readonly string[];

// All sections optional.
colors?: {
  light?: Readonly<Record<string, string>>;
  dark?:  Readonly<Record<string, string>>;
};
geometry?: { radius?, spacing?, fontSans?, fontMono?,};
borders?:  { width?, style? };
motion?:   { duration?, easing? };
focus?:    { width?, offset? };

components?: Partial<Record<ComponentName, Record<string, string>>>;
}

Each theme is exported by name from @brika/clay/themes so a single-theme import drops the rest at build time. The full ordered list, builtInThemes and builtInThemesById, lives behind an opt-in @brika/clay/themes/registry entry, so pickers can pull every preset and one-theme apps don’t have to. Eleven palette themes (nord, ocean, forest, sunset, lavender, ruby, solarized, candy, dracula, mono, clay) plus five showcase themes (brutalist, editorial, terminal, skeuomorph, glass).

05Persist

Remembering the choice

Clay doesn’t manage storage. Your app decides where the active theme id lives, localStorage, a cookie, a user-prefs API, anywhere, and calls applyTheme with the matching preset. See Theming → Persist for a React provider + SSR boot script recipe you can drop into your tree.