Write your own theme
The theme file
Section titled “The theme file”A theme is one JSON file. Start from a built-in with extends and override only what changes: colours per mode, a few tokens, and recipes for the parts you want to look different.
{ "$schema": "https://ui.exponential.at/schemas/theme/v1.json", "id": "brand", "name": "Brand", "extends": "neutral", "modes": { "light": { "color": { "primary": "#4f46e5", "primaryForeground": "#ffffff", "ring": "#6366f1" } }, "dark": { "color": { "primary": "#818cf8", "primaryForeground": "#1e1b4b", "ring": "#818cf8" } } }, "tokens": { "radius": { "md": 10, "lg": 14 }, "type": { "family": { "sans": "Inter" } } }, "fonts": { "Inter": { "fallback": "ui-sans-serif, system-ui", "weights": [400, 500, 600, 700], "source": "host" } }, "recipes": { "Button": { "root": [ { "style": { "borderRadius": "$radius.full" } }, { "when": { "variant": "outline", "state": "hover" }, "style": { "backgroundColor": "$color.accent" } } ] }, "Card": { "root": [{ "style": { "borderWidth": 0, "boxShadow": "$shadow.md" } }] } }}The $schema gives your editor completion and validation (theme.schema.json).
Tokens
Section titled “Tokens”Tokens are the named values every component uses: $color.primary, $spacing.md, $radius.lg, type sizes and line heights, control heights, shadows, borders and motion. Colours and shadows live under modes.light and modes.dark; everything else under tokens. Colours are #rrggbb or #rrggbbaa only; the theme builder converts oklch, hsl and rgb when it imports a shadcn globals.css.
Fonts are referenced by family name. Each platform registers the files (the host’s job), and fonts says what to fall back to.
Recipes
Section titled “Recipes”A recipe styles one part of a component (Button.root, Switch.thumb, Tabs.tab) as a list of rules. A rule applies when every entry of its when matches: recipe props like variant and size, and states (hover, pressed, focus, disabled, checked, open, selected).
Rules merge by specificity: one point per condition, later wins on ties. A child theme’s base rule therefore keeps its parent’s hover and variant rules in force. The Themes reference lists every component’s parts, and the theme builder shows them live.
Load it
Section titled “Load it”Themes load at runtime, so the file can come from your bundle, a URL or an app package. Every loader flattens the extends chain and reports every problem at once (an unknown token, a key outside the whitelist, a misspelled part) and keeps running.
| Platform | Load |
|---|---|
| React | <HostSurface theme={brandJson} … /> |
| Solid | <ExponentialSurface theme={brandJson} … /> |
| Web component | <exponential-surface theme="orange"> · el.theme = brandJson |
| TypeScript | validateTheme(json) · loadTheme(json, { themes: BUILTIN_THEMES }) |
| SwiftUI | ThemeHandle.load(json:) · model.setTheme(…) |
| Compose | ThemeHandle.load(json, parents) · model.setTheme(…) |
| gpui / Rust | eui_core::theme::load_theme(&json, &options) |
Compiled in CI
Section titled “Compiled in CI”bun packages/exponential-ui/guides/check.ts theme runs validateTheme and loadTheme over guides/theme and the built-ins and fails on any issue.