Skip to main content

Persistent chrome on Leptos islands

This guide documents the workspace chrome persistence technique used in examples/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:
each remount still starts from 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

Soft navigation for workspace → workspace:
  1. Intercept same-origin clicks (capture phase).
  2. fetch(url, { headers: { "Islands-Router": "true" } }).
  3. Parse HTML; require #workspace-shell on both sides.
  4. Replace only:
    • #workspace-content
    • #workspace-primary-nav
    • #workspace-topbar-title
  5. Hydrate new leptos-island nodes in those regions (window.__hydrateIsland).
  6. history.pushState / replaceState + fire nav-active event.
Chrome foot islands are never touched, so their Leptos state stays alive.

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.rsinitWorkspaceChromePersist (inline JS exported to WASM via #[wasm_bindgen(inline_js = ...)]). Installed once from a small shell island:
Core navigation rules:
  • 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 stopImmediatePropagation in 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.
Hydrate only unhydrated islands after the swap:

Cache-first chrome data (layer A)

Shared snapshot for session + memberships (used by org switcher, account menu, settings nav):
  • In-memory thread_local (WASM)
  • sessionStorage key (e.g. workspace-chrome-v1) for remounts within the tab
  • Coalesced refresh so three islands do not stampede /api/ui/*
Pattern:
  1. SSR: stable non-pulse shell (or better: SSR real labels from the cookie).
  2. Hydrate: if cache hit → set_snapshot(Ok(cached)) immediately.
  3. Background: refresh_workspace_chrome_cache() → update if changed.
  4. Client active state: data-nav + mark_active_nav(pathname) (no Router in island context).
See 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:
Re-run after soft-nav via workspace-nav-mark (dispatched from pushState hooks and chrome-persist).

What not to do

Verification checklist

After changing chrome:
  1. Hard refresh on /dashboard (cold hydrate).
  2. Tag islands in DevTools (dataset.probe = "1").
  3. Click Organizations, Profile, a settings section.
  4. Assert same leptos-island nodes for theme / account / org (probe still set).
  5. Assert #workspace-content text changed.
  6. Assert zero animate-pulse under #workspace-chrome-foot.
  7. Confirm console has no hydrate errors.
Playwright sketch:

Relation to the fullstack template

Product files dual-sync to the CLI template:
Generated apps from 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)
Prefer a full SPA shell only if you need client-only nested routers without SSR HTML swaps. For most SaaS dashboards on islands, content-only soft-nav + cache-first chrome data is the right balance.

Reference files

See also