Themes
Sixteen first-party presets ship with Clay, eleven palette themes plus five showcase themes that retune geometry, borders, typography, motion, and translucency.
Pick a theme
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.
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.
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();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.
import { ocean, ThemeScope } from "@brika/clay/themes";
<ThemeScope theme={ocean} mode="light">
<Button>Ocean button</Button>
<Card>Ocean card</Card>
</ThemeScope>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>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.
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.
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).
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.