Theming the React Frontend
The React frontend themes with the same semantics and the same persistence contract as the Blazor portal's <FluentDesignTheme> — so a user switching between the two frontends on one origin keeps a single preference. Everything lives in clients/react/src/theme/.
The shared contract with Blazor
Four points of deliberate compatibility (theme/themeMode.ts):
- Three modes —
"system"(default),"light","dark"— Blazor'sDesignThemeModes. - Same storage key, same JSON shape. The preference persists in localStorage under the key
"theme"(the Blazor portal's<FluentDesignTheme StorageName="theme">inSiteSettingsPanel.razor), as the same JSON object the fluent-design-theme web component writes:{ mode, primaryColor? }. Writing the mode preserves sibling fields a Blazor portal may have stored under the same key:
export const DEFAULT_THEME_STORAGE_KEY = "theme";
// Reads tolerate the Blazor JSON shape ({ mode, primaryColor }), a bare string
// ("dark"), or nothing/garbage (→ "system", the Blazor default).
export function readStoredThemeMode(storageKey?: string): ThemeMode;
// Writes keep { ...existing, mode } — primaryColor and friends survive.
export function writeStoredThemeMode(mode: ThemeMode, storageKey?: string): void;
// Blazor's "Reset settings" equivalent — clears the key, back to system.
export function clearStoredTheme(storageKey?: string): void;
systemfollows the OS live —prefers-color-schemeis observed, so an OS-level switch restyles a running app without a reload.- The resolved mode is mirrored onto the document —
document.body.dataset.theme(the samebody[data-theme]attribute Blazor's theme script sets) plusdocument.documentElement.style.colorScheme, so scrollbars and native inputs restyle too.
Design-token propagation
The Fluent design tokens (--colorNeutralBackground1, --colorBrandBackground2, …) are applied by <FluentProvider> from webLightTheme / webDarkTheme — the React v9 equivalent of Blazor's design-token stylesheet. Every control in the pack styles itself from tokens, so the whole tree restyles from the one switch; custom controls should do the same (color: "var(--colorNeutralForeground3)", never hard-coded colors).
export function resolveThemeMode(mode: ThemeMode, prefersDark: boolean): ResolvedThemeMode;
export function fluentThemeFor(resolved: ResolvedThemeMode): Theme; // webDarkTheme | webLightTheme
useThemeMode — the hook
import { useThemeMode } from "@meshweaver/react";
function Shell() {
const { mode, resolved, theme, setMode } = useThemeMode();
// mode: the user's choice — "light" | "dark" | "system"
// resolved: what's actually on screen — "light" | "dark"
// theme: the Fluent theme object for <FluentProvider>
return <FluentProvider theme={theme}>…</FluentProvider>;
}
The hook keeps every instance in sync: instances in the same document coordinate through a custom meshweaver:theme-change event (a toggle in the header and an appearance panel never disagree), and other tabs follow via the browser's storage event.
MeshAreaView calls useThemeMode internally — when you don't pin a theme prop, the view follows the persisted preference automatically. themeStorageKey overrides the storage key for apps that must not share the Blazor preference.
ThemeToggle — the switcher
theme/ThemeToggle.tsx is the React counterpart of the theme selector in the Blazor portal's site settings panel: a menu button offering Light / Dark / System with the current mode checked, persisting through useThemeMode:
import { ThemeToggle } from "@meshweaver/react";
<header>
…
<ThemeToggle /> {/* optional: <ThemeToggle storageKey="my-app-theme" /> */}
</header>
The clients/portal app shell places it in the header next to the user avatar — the same spot the Blazor portal exposes its theme control.
Related
- React Frontend overview
- Getting Started — the
mw-frontendcookie follows the same client-side-preference pattern as the theme.