Documentation
Themes
A theme is one object of design tokens. The framework compiles it to CSS custom properties, so the entire visual identity of a site is a single file you can diff, review, and swap.
Anatomy of a theme
A theme is a small package — either a directory inside your repo or something published to npm and shared across sites. Four files, one of which does the actual work.
themes/my-theme/
package.json # name: cms-theme-my-theme
index.js # imports the manifest + side-effect imports theme.css
theme.config.js # the token manifest — the whole design system
theme.css # font @imports and anything tokens don't coverGenerate one
The scaffolder writes a complete manifest with every token block populated, which is easier to edit down than to build up.
npx cms-create-theme my-themeThe token manifest
Tokens are grouped by role rather than by value — palette, text, surfaces, typography, ctas, chips, focus, radii, elevation, and a utility block for gradients and one-off cases. Naming by role is what makes a theme swappable: components ask for the card surface, not for #0f1620.
const ink = '#0a1224';
const accent = '#c9750b';
export default {
slug: 'my-theme',
meta: { name: 'My Theme', version: '0.1.0' },
tokens: {
palette: { primary: ink, accent, neutral: '#faf7f1' },
text: { primary: ink, muted: '#5b6478', accent, onAccent: '#fff' },
surfaces:{ base: '#faf7f1', card: '#fff', border: 'rgba(10,18,36,0.14)' },
typography: {
bodyFamily: '"Inter", system-ui, sans-serif',
headingFamily: '"Fraunces", Georgia, serif',
monoFamily: '"JetBrains Mono", monospace',
},
ctas: { primary: { bg: accent, text: '#fff' } },
focus: { outline: ink, ring: '0 0 0 2px rgba(10,18,36,0.45)' },
radii: { sm: '6px', md: '12px', lg: '20px', pill: '999px' },
},
assets: { css: './theme.css' },
};Tokens become CSS variables
The framework scopes every token to the active theme's data attribute, so multiple themes can coexist in one bundle without collision. Your components consume the variables directly — including site-local components you write yourself.
:root[data-site-theme="my-theme"] {
--brand-primary: #0a1224;
--brand-accent: #c9750b;
--theme-surface-card: #ffffff;
--brand-font-heading: "Fraunces", Georgia, serif;
/* ...one variable per token */
}theme.css
Everything the manifest can't express goes here: font imports, focus-visible rules, and any override of framework component styling. It's imported as a side effect so the browser applies it as a synchronous stylesheet during the first paint rather than injecting it after hydration — which is what keeps a dark theme from flashing white on load.
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300..700&display=swap');
:root[data-site-theme="my-theme"] {
--brand-ink: #0a1224;
}
:where(a, button):focus-visible {
outline: 2px solid var(--brand-ink);
outline-offset: 3px;
}Activating and validating
Register the theme in vite.config.js and name it in site.json. The validator checks a manifest against the schema and reports missing or malformed token blocks before you find them visually.
# vite.config.js -> themes: ['cms-theme-my-theme']
# site.json -> "theme": "my-theme"
npx cms-validate-themes --site-dir ./site