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.
provide_theme
Mode: dark — panel uses theme_css_vars
Mode: light — 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 <Show> 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)Toggle mode
Use <Show when={dark}> 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>
}