Files
skills/docs/ui/react/react-ui/src/theme/theme-provider/theme-provider.md
2026-05-29 08:20:56 +01:00

3.7 KiB

<ThemeProvider> — chassis-level theme registry

(no submodules)

Purpose

Mounts the active theme registry, hands it down through React context, applies the active theme's tokens to <html> as inline CSS variables, listens for prefers-reduced-motion changes, and prevents flash-of- unstyled-content via an inline FOUC-prevention <script> tag rendered into the <head>. The Provider is the only piece of the chassis that mutates document.documentElement directly — every other consumer reads through the useTheme / useThemeTokens / useThemedVariants hooks.

Features

  • Validates every supplied ThemePlugin against ThemePluginSchema at mount; throws ReactUiError(REACT_UI.THEME_TOKEN_MISSING) on failure
  • Resolves the initial theme from localStorage[storageKey] first, then defaultTheme; throws ReactUiError(REACT_UI.THEME_NOT_REGISTERED) when the resolved name is not registered
  • Inline FOUC-prevention <script> injected before children render so <html data-theme="..."> is set before React hydrates; CSP nonce honoured via scriptNonce
  • Live prefers-reduced-motion listener that updates the live --motion-duration-* variables according to the active theme's respectReducedMotion policy ('strict' collapses to 0ms, 'soft' halves, 'off' ignores)
  • setTheme wrapped in useTransition() so theme switches are low-priority React work that does not block urgent input
  • OTel telemetry: counter react_ui.theme.switch_total{from, to} and histogram react_ui.theme.switch_duration_ms

Usage: Basic example

import { ThemeProvider } from "@suaveplan/react-ui/theme/theme-provider";
import { darkTheme, lightTheme } from "@suaveplan/react-ui/theme";

<ThemeProvider themes={[lightTheme, darkTheme]} defaultTheme="light">
    <App />
</ThemeProvider>;

Usage: Advanced example

import { ThemeProvider } from "@suaveplan/react-ui/theme/theme-provider";
import {
    darkTheme,
    highContrastTheme,
    lightTheme,
} from "@suaveplan/react-ui/theme";

<ThemeProvider
    themes={[lightTheme, darkTheme, highContrastTheme]}
    defaultTheme="system"
    storageKey="my-app-theme"
    enableSystem={true}
    attribute="data-theme"
    scriptNonce={cspNonce}
>
    <App />
</ThemeProvider>;

API Reference

  • <ThemeProvider> — props per ThemeProviderProps:
    • themes: ReadonlyArray<ThemePlugin> — required
    • defaultTheme: string — required; matched against themes[i].name or the literal 'system' (when enableSystem)
    • storageKey?: string — defaults to 'suaveplan-theme'
    • enableSystem?: boolean — when true, allows defaultTheme: 'system' to map onto prefers-color-scheme
    • attribute?: string — defaults to 'data-theme'
    • scriptNonce?: string — CSP nonce for the inline FOUC script
    • children: ReactNode
  • buildFoucScript(input) — pure builder for the inline script body; exposed so tests can assert without rendering
  • halveDuration(input) — pure helper that halves an Xms / Ys duration string; returns '0ms' on parse fail
  • toCssVar(name) — pure helper converting cardForeground → --card-foreground

Implementation notes

The FOUC script is intentionally compact, self-contained, and uses String.prototype.replace for all escaping rather than JSON.stringify so the emitted body cannot smuggle a stray </script> close tag. The useTransition() wrapper around setTheme is the reason the isPending flag exists on useTheme() — consumers that show a tiny spinner during a theme transition read that flag. The Provider tracks the previous theme name in a useRef so the OTel from attribute is accurate even when several setTheme calls land back-to-back.