Documentation

Docs

Introduction

What is rune

Philosophy

Why rune

Architecture

Getting Started

Installation

Quick start

Project structure

The dev loop

Tutorial: build an app

1. Create the app

2. Pages & routes

3. Layouts

4. State with Mimir

5. SSR & data

6. API routes

7. Styling

8. Build & deploy

Core Concepts

The application object

Rendering pipeline

Hydration

The build manifest

Configuration (ekko.json)

Permissions

Conventions

Routing

File-based routing

Dynamic routes

The router (useRouter)

Navigation & Link

Guards & redirects

Programmatic routes

Server-Side Rendering

Overview

The ssr() function

Strategies (eager/lazy)

SSR → hydration

Caching & invalidation

SEO

Mimir, state management

Overview

Atoms

Reading & writing

Selectors (derived state)

Subscriptions & the store

SSR & hydration

Persistence & sessions

Patterns & recipes

Pitfalls

Pages & Layouts

Pages

Layouts

Error & not-found

API Routes

Defining routes

Request & response

Middleware

helmet

cors

rateLimit

bodyLimit

validateContentType

csrf

requestId

timeout

errorHandler

httpsRedirect

secureCookies

ipFilter

safePath

Validation & options

Styling & Theming

SCSS

Theming (light/dark)

Asgard integration

No flash (no-FOUC)

Building & Deploying

The build

Static assets

Production deploy

API Reference

ekko:rune

ekko:rune/router

ekko:rune/mimir

ekko:rune/seo

ekko:ssr / css

ekko.json schema

CLI commands

Guides

Rune app from scratch

Recipes

Dark mode

Forms

Data fetching

Authentication

Pagination

FAQ (use cases)

Documentation

Docs

Introduction

What is rune

Philosophy

Why rune

Architecture

Getting Started

Installation

Quick start

Project structure

The dev loop

Tutorial: build an app

1. Create the app

2. Pages & routes

3. Layouts

4. State with Mimir

5. SSR & data

6. API routes

7. Styling

8. Build & deploy

Core Concepts

The application object

Rendering pipeline

Hydration

The build manifest

Configuration (ekko.json)

Permissions

Conventions

Routing

File-based routing

Dynamic routes

The router (useRouter)

Navigation & Link

Guards & redirects

Programmatic routes

Server-Side Rendering

Overview

The ssr() function

Strategies (eager/lazy)

SSR → hydration

Caching & invalidation

SEO

Mimir, state management

Overview

Atoms

Reading & writing

Selectors (derived state)

Subscriptions & the store

SSR & hydration

Persistence & sessions

Patterns & recipes

Pitfalls

Pages & Layouts

Pages

Layouts

Error & not-found

API Routes

Defining routes

Request & response

Middleware

helmet

cors

rateLimit

bodyLimit

validateContentType

csrf

requestId

timeout

errorHandler

httpsRedirect

secureCookies

ipFilter

safePath

Validation & options

Styling & Theming

SCSS

Theming (light/dark)

Asgard integration

No flash (no-FOUC)

Building & Deploying

The build

Static assets

Production deploy

API Reference

ekko:rune

ekko:rune/router

ekko:rune/mimir

ekko:rune/seo

ekko:ssr / css

ekko.json schema

CLI commands

Guides

Rune app from scratch

Recipes

Dark mode

Forms

Data fetching

Authentication

Pagination

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

  1. The server renders the page with the theme atom's default (say "dark") and inlines the CSS.
  2. The browser paints that HTML, dark.
  3. The hydration bundle loads, reads the user's persisted choice ("light"), and toggles the class.
  4. 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:

1
2
3
// server.tsx
const noFouc = `<script>try{if(localStorage.getItem('site-theme')!=='light')document.documentElement.classList.add('dark');}catch(e){}</script>`;
const head = `${noFouc}${seo.headTags()}`; // your global stylesheet is injected by the build (imported in layout)

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 .dark unless 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():

1
2
3
export function ssr() {
return { title: "Home", __atoms: { "site-theme": "dark" } };
}

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:

1
2
3
// in ThemeBridge's apply():
document.documentElement.classList.toggle("dark", v !== "light");
try { localStorage.setItem("site-theme", v); } catch {}

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

LayerFixesMechanism
no-FOUC inline scriptthe first paintsets .dark synchronously from localStorage before paint
ssr().__atoms seedthe hydrated React treeserver renders with the right theme value
ThemeBridge + sessionsubsequent changes + reloadstoggles 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.