theme system
what is this?
the site supports 5 color themes and 3 typography options. themes are implemented with css custom properties, no javascript theme-specific styles. switching is instant, no flash of wrong theme (fouc).
available themes
| name | type | base palette |
|---|---|---|
| hollow knight | dark | catppuccin mocha (yellow primary) |
| radiant | light | catppuccin latte (yellow primary) |
| tokyo night | dark | tokyo night colors (cyan accent) |
| dracula | dark | dracula palette (pink accent) |
| one dark | dark | atom one dark (blue accent) |
typography options: mono (jetbrains mono), sans (inter), serif (merriweather)
architecture
src/styles/├── themes/│ ├── hollowknight.css # --hk-* raw palette│ ├── radiant.css # --rad-* raw palette│ ├── tokyo-night.css # --tn-* raw palette│ ├── dracula.css # --drac-* raw palette│ └── onedark.css # --od-* raw palette├── tokens.css # semantic tokens + theme selectors└── global.css # imports everythingeach theme file defines raw palette variables. tokens.css maps them to semantic tokens.
theme file structure
theme files only contain raw color values:
/* hollowknight.css - catppuccin mocha */:root { --hk-base: #1e1e2e; --hk-text: #cdd6f4; --hk-yellow: #f9e2af; --hk-blue: #89b4fa; /* ... */}prefixes: --hk- (hollowknight), --rad- (radiant), --tn- (tokyo-night), --drac- (dracula), --od- (onedark)
semantic tokens
tokens.css defines semantic tokens that components use:
@theme { /* fallback (radiant/light as base) */ --color-global-bg: var(--rad-base); --color-global-text: var(--rad-text); --color-primary: var(--rad-yellow); --color-accent: var(--rad-blue); --color-link: var(--rad-sky);
--color-surface-0: var(--rad-base); --color-surface-1: var(--rad-mantle); --color-surface-2: var(--rad-surface0);
--color-border-default: var(--rad-surface1); /* ... */}then per-theme overrides:
html[data-theme="hollowknight"] { --color-global-bg: var(--hk-base); --color-global-text: var(--hk-text); --color-primary: var(--hk-yellow); /* ... */}
html[data-theme="tokyo-night"] { --color-global-bg: var(--tn-base); --color-primary: var(--tn-cyan); /* ... */}theme provider
ThemeProvider.astro handles fouc prevention and persistence:
<script is:inline>(function() { const VALID_THEMES = ["hollowknight", "radiant", "tokyo-night", "dracula", "onedark"]; const stored = localStorage.getItem("theme");
if (stored && VALID_THEMES.includes(stored)) { document.documentElement.dataset.theme = stored; } else { // system preference fallback const prefersDark = matchMedia("(prefers-color-scheme: dark)").matches; document.documentElement.dataset.theme = prefersDark ? "hollowknight" : "radiant"; }})();</script>this runs before paint (inline in <head>), preventing flash.
typography switching
typography is simpler - just override --font-body:
html[data-typography="mono"] { --font-body: var(--font-mono);}html[data-typography="sans"] { --font-body: var(--font-sans);}html[data-typography="serif"] { --font-body: var(--font-serif);}theme panel component
ThemePanel.astro provides the dropdown ui. stores selections in localStorage:
localStorage.setItem("theme", "tokyo-night");localStorage.setItem("typography", "sans");changes apply immediately by updating data-theme and data-typography attributes on <html>.
expressive code integration
code blocks use catppuccin themes. the css selector maps all dark themes to mocha, light to latte:
const darkThemes = new Set([ "hollowknight", "tokyo-night", "dracula", "onedark",]);
export const expressiveCodeOptions = { themeCssSelector(theme) { const isDarkTheme = theme.type === "dark"; return isDarkTheme ? [...darkThemes].map((t) => `[data-theme="${t}"]`).join(", ") : `[data-theme="radiant"]`; }, themes: ["catppuccin-mocha", "catppuccin-latte"],};adding a new theme
- create
src/styles/themes/{name}.csswith raw palette - add theme info to
themeConfiginsite.config.ts - add
ThemeNametype intypes.ts - add
html[data-theme="{name}"]selector intokens.css - add to
VALID_THEMESinThemeProvider.astro - if dark, add to
darkThemesset for expressive-code - if dark, add to color-scheme selector in
global.css
types
type ThemeName = | "hollowknight" | "radiant" | "tokyo-night" | "dracula" | "onedark";type Typography = "mono" | "sans" | "serif";
interface ThemeInfo { name: ThemeName; label: string; isDark: boolean;}
interface ThemeConfig { themes: ThemeInfo[]; defaultDark: ThemeName; defaultLight: ThemeName; typography: Typography[]; defaultTypography: Typography;}config
export const themeConfig: ThemeConfig = { themes: [ { name: "hollowknight", label: "hollow knight", isDark: true }, { name: "radiant", label: "radiant", isDark: false }, { name: "tokyo-night", label: "tokyo night", isDark: true }, { name: "dracula", label: "dracula", isDark: true }, { name: "onedark", label: "one dark", isDark: true }, ], defaultDark: "hollowknight", defaultLight: "radiant", typography: ["mono", "sans", "serif"], defaultTypography: "mono",};