Skip to main content

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/<slug>/
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 cover

Generate one

The scaffolder writes a complete manifest with every token block populated, which is easier to edit down than to build up.

terminal
npx cms-create-theme my-theme

The 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.

themes/my-theme/theme.config.js
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.

generated at build time
: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.

themes/my-theme/theme.css
@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.

terminal
# vite.config.js  ->  themes: ['cms-theme-my-theme']
# site.json       ->  "theme": "my-theme"

npx cms-validate-themes --site-dir ./site