Documentation
Docs
Introduction
Getting Started
Tutorial: build an app
Core Concepts
Routing
Server-Side Rendering
Mimir, state management
Pages & Layouts
API Routes
Styling & Theming
Building & Deploying
API Reference
Guides
Recipes
FAQ (use cases)
Theming
A theme in rune is two things working together: CSS custom properties flipped by a class on <html>, and
a Mimir atom that is the single source of truth for which theme is active. Get those two in sync and
dark/light "just works", including across navigations, reloads, and the first paint.
The single source of truth: an atom
Everything that needs to know the theme reads this atom; everything that changes the theme sets it. Because it is an atom (not component state), the choice survives client navigation, and with a session it survives a reload (see Persistence & sessions).
The CSS side: tokens + the .dark class
Define every colour twice, once on :root (the light default) and once on .dark (the override):
Components only ever read var(--text), var(--bg), etc. Adding or removing the .dark class on <html>
re-themes the entire app, no component-level theme logic.
The bridge: atom → .dark class
A tiny client component subscribes to the atom and toggles the class on <html> (and starts the persistence
session). Render it once in the root layout so it runs on every page:
The toggle
No flash on first paint
Two more touches make the first paint correct (no flash of the wrong theme):
- Seed the atom in
ssr().__atomsso the React tree matches. - Set the
.darkclass before paint with a tiny inline script reading the mirroredlocalStoragevalue.
Both are covered in No-FOUC. The atom keeps React right; the script keeps the very first paint right; the session keeps it across reloads.
"One palette, two declarations" when using a component library
If you also use @ekko/asgard (its components are themed by a JS theme object, not your CSS variables), the
brand colours must be declared in two places kept in sync:
- the SCSS tokens (
:root/.dark) that style your own markup, and - the Asgard theme objects (e.g.
lib/theme.ts) that style Asgard components.
The atom is still the single source of truth for which theme is active; both consumers read it. When you change a brand colour, change it in both declarations, or your chrome and the Asgard components will drift. See Asgard integration.
Recap
| Piece | Role |
|---|---|
themeAtom | the source of truth (which theme) |
:root / .dark SCSS tokens | the colours for each theme |
ThemeBridge | applies the class, starts the session, mirrors to localStorage |
ssr().__atoms seed | correct React tree on first render |
| no-FOUC inline script | correct first paint |
Next: using the Asgard component + docs UI, Asgard integration.