Theming

The 19 built-in themes, runtime switching, and how to add your own palette.

Theming

ShockStack ships 19 curated themes out of the box. Switching between them is a one-line operation at runtime: set data-theme="<name>" on <html>. Every component reacts instantly because colors flow from design tokens.

The model

┌───────────────────────────────┐
│  <html data-theme="dracula">  │
│    ├── tokens.css (all themes scoped by data-theme)
│    ├── @theme block in global.css
│    └── semantic Tailwind utilities
└───────────────────────────────┘

Three moving parts:

  1. Tokenspackages/tokens/dist/tokens.css contains a block per theme: [data-theme="dracula"] { --ss-color-bg-primary: … }.
  2. Theme attribute<html data-theme="midnight"> picks which block wins.
  3. Semantic utilities — components never name a color directly; they reach for bg-bg-primary, text-fg-muted, etc.

The 19 built-in themes

dracula — classic purple/pink dark
light — bright companion (Alucard)
nord — arctic blue palette
gruvbox — warm retro dark
gruvbox-light — warm retro light
gruvbox-soft — lower-contrast gruvbox
gruvbox-hard — high-contrast gruvbox
catppuccin-mocha — pastel dark
tokyo-night — neon city dark
one-dark — Atom-inspired
solarized-dark — high-contrast science
solarized-light — light sibling
kanagawa-wave — painterly ocean dark
rose-pine-moon — soft dusk palette
everforest-dark — earthy low-glare dark
ayu-mirage — warm crisp editor dark
github-dark-dimmed — familiar dimmed developer UI
midnight — default dark
dawn — default light

The canonical list lives in frontend/src/lib/themes.ts (VALID_THEMES). DEFAULT_THEME is midnight; the built-in ThemeToggle and PagesMenu components offer the full set.

Resolution order

When the document loads, theme selection follows:

  1. Saved preferencelocalStorage.getItem("theme") if it matches a known theme.
  2. Default themeDEFAULT_THEME from src/lib/themes.ts.

The bootstrap script in BaseLayout.astro runs before paint to prevent a flash of wrong theme.

Switching themes at runtime

The ThemeToggle.vue component handles persistence and View Transitions animation. If you need to switch imperatively:

import { applyTheme } from "../lib/themes";

applyTheme("tokyo-night"); // sets data-theme + writes localStorage

For a one-off, a DOM update is enough:

document.documentElement.setAttribute("data-theme", "dawn");
localStorage.setItem("theme", "dawn");

Adding a new theme

Add a theme

bash

$ cp packages/tokens/tokens/dracula.json packages/tokens/tokens/my-theme.json

edit my-theme.json with your values

$ pnpm tokens:build

Then:

  1. Register the new theme in packages/tokens/build.ts (it becomes a [data-theme="my-theme"] block).
  2. Add it to VALID_THEMES in frontend/src/lib/themes.ts so the picker knows about it.
  3. Add a preview entry in THEME_OPTIONS if you want it in the UI switcher.

Accessibility

  • Contrast — each semantic token pair (bg-primary / fg-primary) is tuned to meet WCAG AA in every theme. If you adjust a value, re-test.
  • Reduced motion — the view-transition animation on theme switch is skipped when prefers-reduced-motion: reduce.
  • Focus outlines — derive from --color-accent-purple, which exists in every theme.

Semantic over literal

In code: always use the semantic utility.

<!-- ✅ Reacts to every theme -->
<div class="bg-bg-secondary text-fg-primary border border-border-default">

<!-- ❌ Locked to one palette -->
<div class="bg-[#1e1e2e] text-[#cdd6f4] border border-[#45475a]">

If you reach for a bracket color, that’s a signal you need a new token.