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)
No flash of the wrong theme
FOUC, a "flash of unstyled (or wrong-themed) content", is the brief moment where a page paints with the default theme before JavaScript switches it to the user's choice. rune apps eliminate it with a two-part pattern. This page is that pattern, in full.
Why the flash happens
- The server renders the page with the theme atom's default (say
"dark") and inlines the CSS. - The browser paints that HTML, dark.
- The hydration bundle loads, reads the user's persisted choice (
"light"), and toggles the class. - The page repaints, light. The user saw a flash of dark.
The fix is to make steps 1 and 2 already correct, before any React runs.
Part 1 , set the class before paint (the no-FOUC script)
Inject a tiny synchronous inline script into the document head that sets the .dark class from a fast,
synchronous source (localStorage) before the browser paints:
Because it is inline and synchronous in <head>, it runs before the body paints. The first paint already has
the right .dark state, so the CSS variables resolve to the right colours immediately. No flash.
Default-to-dark vs default-to-light: the example adds
.darkunless the saved value is"light". Flip the condition for a light-default site. The point is to read the persisted choice synchronously and set the class before paint.
Part 2 , seed the atom (so React agrees)
The script fixes the CSS; you also need the React tree to render with the right theme so hydration
does not mismatch. Seed the theme atom in ssr():
On a cached static page, seed only a neutral default (the page is shared); the client's persisted session takes over after hydration. See Mimir → SSR & hydration for the cached-page caveat.
Part 3 , keep localStorage in sync
The no-FOUC script reads localStorage, so your bridge must write it whenever the theme changes:
Now the loop is closed: the user toggles → the atom changes → the bridge updates the class and mirrors to
localStorage → on the next load the no-FOUC script reads it synchronously → first paint is correct.
The three layers, together
| Layer | Fixes | Mechanism |
|---|---|---|
| no-FOUC inline script | the first paint | sets .dark synchronously from localStorage before paint |
ssr().__atoms seed | the hydrated React tree | server renders with the right theme value |
ThemeBridge + session | subsequent changes + reloads | toggles the class, persists, mirrors to localStorage |
Use all three and there is no flash, on first load, on navigation, or on reload.
A note on keys
The atom key, the localStorage key, and the no-FOUC script's key must match (here, "site-theme").
If you rename one, rename all three, or the script reads a stale/empty value and the flash returns. (Treat
the key as a stable contract, see Mimir → Pitfalls.)
Beyond theme
The same idea applies to any first-paint-critical preference, reduced motion, a chosen density, an
RTL/LTR direction: read it synchronously in the head script, seed the atom in ssr(), and persist via the
bridge. Theme is just the most common case.
That completes Styling. Next: shipping it, Building → The build.