Persistent chrome on Leptos islands
This guide documents the workspace chrome persistence technique used inexamples/fullstack-app: sidebar org switcher, account menu, and theme toggle
stay the same DOM nodes when the user moves between dashboard, settings, and
account pages.
Use it whenever you build a product shell on Leptos islands + Spin/WASI and
chrome must not look like it is “loading again” on every route change.
The problem
With pure multi-page islands (no client router script), every<a href> is a
full document load. The whole shell re-SSR’s and every #[island] remounts.
Even with a client cache:
None, swaps the view tree, and flickers.
Symptoms:
- Org switcher / account flyout flash skeleton or generic “Workspace” shell
- Theme toggle briefly shows the wrong glyph (default then localStorage)
- Network tab fills with repeated
get_current_session/list_organizations
Two layers of the fix
B is the real fix for flicker. A is still useful for cold load and for when
islands must remount (hard refresh, leaving the shell).
Architecture
- Intercept same-origin clicks (capture phase).
fetch(url, { headers: { "Islands-Router": "true" } }).- Parse HTML; require
#workspace-shellon both sides. - Replace only:
#workspace-content#workspace-primary-nav#workspace-topbar-title
- Hydrate new
leptos-islandnodes in those regions (window.__hydrateIsland). history.pushState/replaceState+ fire nav-active event.
Prerequisites
1. Enable islands router on the shell
islands=true alone is not enough. Without islands_router=true, every hop
is a full document load.
2. Server must honor Islands-Router
With leptos-wasi-runtime / leptos_wasi feature islands-router, the handler
detects the header, provides IslandsRouterNavigation, and can omit redundant
hydration scripts on subsequent navigations.
The fullstack example already enables:
3. Stable region IDs in the shell markup
Mark persistent areas with
data-chrome-persist="true" for documentation and
future tooling.
Optional CSS for smoother transitions:
Implementation sketch (client)
Reference:examples/fullstack-app/src/app/mod.rs → initWorkspaceChromePersist
(inline JS exported to WASM via #[wasm_bindgen(inline_js = ...)]).
Installed once from a small shell island:
- Only intercept when both current and target path are “workspace chrome”
paths (dashboard, account, organizations,
/org/…, admin, …). - Leave login/register/public pages to full navigation.
- Use
stopImmediatePropagationin the capture phase so the default islands-router full-tree diff does not remount chrome when branch markers diverge. - On failure (network, missing shell), fall back to
window.location.href.
Cache-first chrome data (layer A)
Shared snapshot for session + memberships (used by org switcher, account menu, settings nav):- In-memory
thread_local(WASM) sessionStoragekey (e.g.workspace-chrome-v1) for remounts within the tab- Coalesced refresh so three islands do not stampede
/api/ui/*
- SSR: stable non-pulse shell (or better: SSR real labels from the cookie).
- Hydrate: if cache hit →
set_snapshot(Ok(cached))immediately. - Background:
refresh_workspace_chrome_cache()→ update if changed. - Client active state:
data-nav+mark_active_nav(pathname)(no Router in island context).
WorkspaceOrgSwitcher, WorkspaceUserMenu, and WorkspaceSettingsNavLinks
in examples/fullstack-app/src/app/workspace/mod.rs.
Client-side focus for flyouts
Islands do not sit in the Router context. Highlight the current account / settings item with DOM attributes:workspace-nav-mark (dispatched from pushState
hooks and chrome-persist).
What not to do
Verification checklist
After changing chrome:- Hard refresh on
/dashboard(cold hydrate). - Tag islands in DevTools (
dataset.probe = "1"). - Click Organizations, Profile, a settings section.
- Assert same
leptos-islandnodes for theme / account / org (probe still set). - Assert
#workspace-contenttext changed. - Assert zero
animate-pulseunder#workspace-chrome-foot. - Confirm console has no hydrate errors.
Relation to the fullstack template
Product files dual-sync to the CLI template:ddd init --preset fullstack pick up the same shell
regions and initWorkspaceChromePersist when the CLI package is released.
When this technique is enough
- Authenticated product shell with a stable sidebar
- Many routes, same chrome, heavy page islands
- Spin/WASI + islands (not full CSR SPA)
Reference files
See also
- Reactive Leptos UI — optimistic UI patterns for counter-style apps
- WASI auth fullstack — Spin + wasi-auth stack
- Leptos book: Islands