Themes

PureStack generates theme CSS from palettes. Each skin supplies light and dark values for type, surfaces, borders, and semantic tones. Components read those shared values so a theme change stays consistent across the page.

See Semantic tone classes for visual examples of the roles generated from those palettes.

Start with the standard skin

Add this to siteConfig.json:

{
  "style": {
    "fileName": "site.css",
    "href": "/assets/site.css",
    "themes": ["light", "dark"],
    "theme": { "skin": "standard" }
  }
}

The generator writes assets/site.css for light and assets/site.dark.css for dark. Its theme toggle uses the configured theme list and remembers a visitor's choice. Try the ThemeToggle example on this site.

Create a skin

Pass a color object to the standard skin to generate matching light and dark palettes. You can provide any subset of its semantic colors; omitted colors use the standard defaults:

import { definePlugin } from '@purestack/ts-ssg'
import { themeSkins } from '@purestack/ts-style'
import { merge } from '@purestack/ts-util'

export const mySitePlugin = definePlugin({
  name: 'my-site',
  skins: {
    'my-site': {
      create: () => {
        const standard = themeSkins.standard.create({
          accent: '#4b64c8',
          neutral: '#293047',
          secondary: '#168a75',
          feature: '#b04783',
        })
        return {
          light: merge(standard.light, {
            font: { size: { body: '1rem' } },
          }),
          dark: merge(standard.dark, {
            font: { size: { body: '1rem' } },
          }),
        }
      },
    },
  },
})

Add the plugin to your build's plugins, then set "theme": { "skin": "my-site" } in the style config. A skin is a full light/dark pair, and merge preserves palette values you do not override. You can also pass a list of built-in preset names, such as ['green'], to themeSkins.standard.create().

Extend styles with palette roles

Use semantic roles when a custom section needs color, so it follows both themes:

import { styleBuilder, themes } from '@purestack/ts-style'

themes.forEach((theme, palette) => {
  styleBuilder.get(theme).select('.feature-note').css({
    background: palette.semanticTone.info.surface.rest.background,
    color: palette.semanticTone.info.surface.rest.text,
    borderColor: palette.semanticTone.info.surface.rest.border,
  })
})

Register such styles from a plugin's onConfigResolved hook, which runs before the build writes theme assets. For a real example, inspect the Studio plugin, with its skin and styles.