Skip to content

feat(react-ui): add no-flash color scheme architecture - #824

Open
ankit-thesys wants to merge 1 commit into
mainfrom
codex/no-flash-theme
Open

feat(react-ui): add no-flash color scheme architecture#824
ankit-thesys wants to merge 1 commit into
mainfrom
codex/no-flash-theme

Conversation

@ankit-thesys

Copy link
Copy Markdown
Contributor

Fixes #736

Supersedes the approach in closed PR #737. This implementation starts from current main and separates global color-scheme preference from scoped theme-token rendering.

Problem

OpenUI previously selected dark defaults through prefers-color-scheme and injected custom/forced theme variables from useInsertionEffect. A forced or persisted scheme that disagreed with the device could therefore paint the wrong tokens before hydration, and custom themes painted stock values first. Hydration speed changed how visible the mismatch was; it did not remove the mismatch.

Architecture

  • Generate complete light and dark default token rules selected by data-openui-color-scheme, retaining a no-JavaScript prefers-color-scheme fallback.
  • Add an explicit ColorSchemeScript for parser-time selection from forced, server-readable, stored, or system preference.
  • Add a root-only ColorSchemeProvider/useColorScheme store for persistence, system changes, cross-tab synchronization, forced mode, optional transition suppression, and native color-scheme synchronization.
  • Keep ThemeProvider focused on theme objects, SSR-rendered custom-property rules, nested scopes, explicit modes, and portals. Existing omitted-mode usage without the root provider retains the legacy light fallback.
  • Keep browser-only preference values honest during SSR. CSS renders both possibilities; useTheme() retains a deterministic light JavaScript fallback and exposes isModeServerResolved.
  • Move CSS-compatible theme consumers away from hydration-dependent values: syntax highlighting uses selector-driven variants when the server cannot resolve the mode, and chart inline/SVG colors now inherit OpenUI variables.
  • Install the architecture in the production Next.js example and document CSP, cookie/server-mode, storage, nested scope, and SSR limitations.

The detailed research, framework comparison, resolved decisions, security constraints, compatibility requirements, and follow-up instructions live in packages/react-ui/src/components/ThemeProvider/NO_FLASH_THEME_DESIGN.md.

Compatibility

  • Explicit ThemeProvider mode="light" | "dark" remains controlled and server-resolved.
  • Omitted mode remains light unless a parent/root color-scheme provider exists.
  • Nested themes, explicit selectors, independent light/dark overrides, and portal classes retain full reset semantics.
  • React 18 and React 19 server output are supported without unsafe theme-token interpolation.
  • Full, layered, default-only, and per-component CSS artifact contracts remain intact.
  • Local-storage-derived JSX is not presented as server-correct. Applications that need scheme-dependent SSR markup can provide authoritative serverMode/serverSystemMode from a synchronized cookie/request source.

Browser validation

Production Next.js tests held JavaScript chunks to inspect the actual pre-hydration paint:

Scenario Before hydration After hydration
Stored dark, light device root attr dark; custom dark background/text/syntax unchanged; context dark
Stored light, dark device root attr light; custom light background/text/syntax unchanged; context light
System mode, live OS change root tokens follow emulated scheme provider and context update together
Cross-tab storage event root switches to incoming preference context matches
JavaScript disabled, dark device no root attr; dark media fallback visible not applicable
Strict nonce CSP, stored dark bootstrap and theme styles accepted; dark first paint zero CSP console errors

Measured cost

  • Generated defaults: 23,832 -> 35,619 bytes uncompressed; 3,135 -> 3,446 bytes gzip (+311 bytes).
  • Built default bootstrap script: 873 bytes; 393 bytes gzip.
  • Inherited root provider with no custom overrides: no theme style node.
  • One custom override per scheme: 568 bytes of server-rendered style text.
  • Explicit full dark scope: 13,160 bytes, preserving the historical complete reset contract.

Verification

  • pnpm --filter @openuidev/react-ui run ci (43 tests; format and lint pass, with only existing repository warnings)
  • pnpm --filter @openuidev/react-ui typecheck
  • pnpm --filter @openuidev/react-ui build (53 CSS artifacts verified)
  • pnpm --filter @openuidev/react-ui check:publint
  • pnpm --filter @openuidev/react-ui check:attw
  • pnpm --filter openui-chat lint
  • pnpm --filter openui-chat build (Next.js production SSG/SSR)
  • pnpm --filter openui-fastapi-frontend build (Vite 6 production build)
  • Packed-package React 18 SSR fixture
  • Playwright production-browser first-paint, hydration, no-JS, storage, system-change, selector, and strict-CSP probes

@ankit-thesys
ankit-thesys force-pushed the codex/no-flash-theme branch from c28ea82 to 97035b0 Compare July 22, 2026 23:46
Comment on lines -338 to -345
const { mode, theme: userTheme } = useTheme();

const barInternalLineColor = useMemo(() => {
if (mode === "light") {
return "rgba(255, 255, 255, 0.3)";
}
return "rgba(0, 0, 0, 0.3)";
}, [mode]);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we cant remove this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dark only apps render the stock light theme on light scheme devices because there is no scheme pinned CSS export

1 participant