For an AI

Theme

The Theme menu in this header is the real API: FlowApp::with_html_theme + data-r-theme buttons inside a <Popup>. Clicking Slate restyles html[data-theme] for the whole site — cookie, localStorage, no hydration, no View Transition around the swap.

LIVE

provide_theme

Mode: dark — panel uses theme_css_vars

Live palettes (whole app)

Do not put the switcher onClick in a layout handler — that chunk is lazy, so the first click waits. with_html_theme injects a blocking head script that listens for [data-r-theme]. Style the selected chip with [aria-pressed=true] or .r-theme-on — do not bake a selected class from the SSR cookie (it stays after a live swap).

FlowApp::new()
    .with_html_theme(
        HtmlTheme::new(["paper", "slate", "midnight"])
            .dark(["midnight"])
            .cookie("my_app_theme")       // default: resuma_theme
            .storage_key("my-app-theme")  // default: resuma-theme
    )

// CSS:
// html[data-theme="slate"] { --bg: #f4efe6; --text: #1c1917; }
// body { background: var(--bg); color: var(--text); }

view! {
    <Popup id="themes">
        <button slot="anchor" type="button">"Theme"</button>
        <ThemeSwitch id="slate">"Slate"</ThemeSwitch>
        <button type="button" data-r-theme="midnight">"Midnight"</button>
    </Popup>
}

Per-response override

set_page_theme("slate") in a page or #[load] sets SSR html[data-theme] for that response and marks the document data-theme-forced. The boot script keeps that palette instead of the visitor's stored pick, and SPA navigation restores the pick when they leave the page. Without a forced theme the cookie / localStorage pick wins.

SPA must not clobber a live pick

NavLink prefetch can be older than the palette the user just chose. The runtime copies dir from the fetched document (RTL survives) but does not copy data-theme — except from a page marked data-theme-forced. Do not wrap the theme swap in document.startViewTransition while a popover is open — Chromium skips the update callback.

Snapshot tokens (SSR / Show)

provide_theme / theme_css_vars bake inline --resuma-* on a wrapper. That snapshot does not update when html[data-theme] changes. Use it for a static shell or a &lt;Show&gt; branch (demo above).

#[layout("/")]
fn AppLayout() -> View {
    provide_theme(Theme {
        mode: "dark".into(),
        primary: "#6366f1".into(),
        background: "#0b1020".into(),
        foreground: "#e6e8ee".into(),
    });

    view! {
        <div class="app" style={theme_css_vars(&use_theme())}>
            <Slot />
        </div>
    }
}

Consume in components

#[component]
fn ThemedButton() -> View {
    let theme = use_theme();
    view! {
        <button style={format!("background: {}", theme.primary)}>
            "Click"
        </button>
    }
}

PWA colors from theme

FlowApp::new()
    .with_theme_pwa(Theme {
        primary: "#c9a962".into(),
        background: "#0a0908".into(),
        ..Default::default()
    })
    .auto_pages("src/pages", PagesRegistry)

PWA & static files.

Toggle mode

Use &lt;Show when={dark}&gt; with two theme_css_vars panels — try the live demo above.

let dark = signal(true);

view! {
    <Show when={dark} fallback={light_panel}>
        <div class="app" style={theme_css_vars(&dark_theme)}>...</div>
    </Show>
}