Skip to content

Fix dark-mode flash on page navigation; respect OS theme on first load - #243

Open
Trosper3 wants to merge 3 commits into
ReduxAPI_GUIfrom
fix/dark-mode-flash-next-themes
Open

Fix dark-mode flash on page navigation; respect OS theme on first load#243
Trosper3 wants to merge 3 commits into
ReduxAPI_GUIfrom
fix/dark-mode-flash-next-themes

Conversation

@Trosper3

@Trosper3 Trosper3 commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Switches the site's light/dark theming from a hand-rolled React context to next-themes, so returning dark-mode users no longer see a flash of light mode on every page load, and first-time visitors get their OS theme instead of a hardcoded light default. Also fixes a related flash that happens when using the light/dark toggle itself.

Features

  • No more flash of the wrong theme when a page loads or reloads — the correct theme is applied before the page is visibly painted, instead of switching a moment after the page appears.
  • First-time visitors now see the site in their operating system's light or dark setting automatically, rather than always starting in light mode.
  • The light/dark toggle button in the top nav bar works exactly as before, and your choice is still remembered across visits.
  • Switching between pages in the app never causes the theme to flicker or change on its own — it only changes when you click the toggle, or on your very first visit while your system preference is being detected.
  • Clicking the light/dark toggle itself no longer causes hoverable items (cards, panels) to briefly flash the wrong color before settling — the color swap now happens instantly instead of visibly animating through every in-between shade.

What to Check

  • Switch to dark mode using the toggle, then click through several different pages — you should never see a flash of the light theme, even for a split second.
  • While in dark mode, reload the page — it should load straight into dark mode, no flash.
  • Open the site fresh (e.g. a private/incognito window, or after clearing site data) with your computer set to dark mode — it should start in dark mode automatically, without you having picked it.
  • Click the light/dark toggle a few times, including while hovering over cards/panels — colors should switch instantly with no flash of the wrong theme, and hover highlighting should still look and feel normal afterward.

Issue Handling

Closes #240

Verification (technical detail)

  • npm run lint and npm run build both pass; no new errors or warnings in any file touched by this change (all lint errors/warnings present in the repo are pre-existing and in unrelated files).
  • Fixed a react-hooks/set-state-in-effect lint error on the mount-detection gate by replacing useState+useEffect with useSyncExternalStore, which gives the same server-false/client-true value without ever calling setState inside an effect.
  • Started the dev server and fetched the rendered HTML directly to confirm the mechanism is wired correctly:
    • next-themes's blocking inline script is present at the very top of <body> on every route checked (/, /browse), and runs before any other content — it reads the stored preference (or resolves prefers-color-scheme if nothing is stored) and sets the dark/no class on <html> synchronously.
    • The critical CSS added to styles/globals.css (html.dark, html.dark body { ... }) compiled correctly into the render-blocking stylesheet <link> loaded in <head>, so it's in effect before the first paint.
    • Confirmed no other code in the app touches document.documentElement's classList or otherwise fights the dark class next-themes sets.
    • Confirmed the old redux-theme-mode localStorage key was read/written only inside the file being replaced, so removing it doesn't silently break anything else.
  • Manually verified in a real browser (2026-09-10): confirmed no flash of the wrong theme when loading or reloading pages, and that light/dark mode stays consistent while navigating across pages.
  • Follow-up fix (2026-09-10): a separate flash was found — toggling the theme (not page load) caused hover-highlighted elements to briefly show the wrong color, because sectionCardSx/innerCardSx (theme.js) use transition: all for a smooth hover effect, and every one of those properties (background/border/shadow) also differs between light/dark variants. Toggling now briefly disables all CSS transitions for one frame around the swap (.theme-transition-off in globals.css, wired into toggleMode in ThemeModeContext.js), so the color change is instant instead of animating through it, while normal hover transitions resume immediately after.
  • One tradeoff worth flagging: because MUI's theme is a JS value that can't be known during server rendering, the app's body content now renders empty in the server-sent HTML until the client mounts and resolves the theme (previously, full content was server-rendered, just with the wrong colors for dark-mode users). This is the standard next-themes/MUI tradeoff and fills in near-instantly after hydration, but it does mean a non-JS crawler would see less raw HTML content than before.

Replaces the hand-rolled ThemeModeContext (which always initialized to
light and only applied a saved dark preference in a post-mount effect,
causing a visible flash for returning dark-mode users on every full
page load) with next-themes. Its blocking inline script sets the theme
class on <html> before React hydrates, and resolves the OS-level
prefers-color-scheme on a visitor's first-ever load instead of
defaulting to light.

ThemeModeContext.js is now a thin MUI bridge: it reads next-themes'
resolvedTheme and builds the MUI theme from it, gated on mount so
server and pre-mount client renders match (no hydration mismatch, and
no MUI component is ever painted with the wrong-mode colors). Critical
CSS in globals.css colors the body correctly during that brief
pre-mount window, keyed off the same html.dark class the blocking
script sets.

The useThemeMode() hook keeps its existing { mode, toggleMode } shape,
so all 17 existing call sites (including the toggle button in
ResponsiveAppBar) work unchanged.
Comment thread components/ThemeModeContext.js Fixed
@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown

Redux Build System — CI Report

npm · commit 4633c74

Operation Status Summary Time
audit 1 high · 1 moderate 0.9s
format-check 60 format · 33 import order 0.5s
lint 24 errors, 41 warnings 7.5s
typecheck ⏭️ no tsconfig.json
unit-test ⏭️ no test script in package.json
build built local/redux_gui:ci · 235MB 58.5s
integration-test 1 failed · 3 passed 17.4s
push not run — integration-test failed

Overall: ❌ 1 passed · 4 failed · 2 skipped · 1 blocked

❌ audit — 1 high · 1 moderate
Severity Location Rule Message
high svgo@3.0.0 - 3.3.4 GHSA-2p49-hgcm-8545 SVGO removeScripts plugin leaves some executable scripts intact
moderate baseline-browser-mapping@>=2.0.0 <2.11.0 GHSA-w5vr-8v7q-w6rv baseline-browser-mapping process termination on invalid input causes denial of service
❌ format-check — 60 format · 33 import order
Severity Location Rule Message
error Tools/Constants.js format needs formatting
error Tools/ProblemInstanceParser.js format needs formatting
error biome.json format needs formatting
error components/ContributorCard.js format needs formatting
error components/Quantum/QuantumCircuitVisualizer.js assist/source/organizeImports Sort these imports.
error components/Quantum/QuantumCircuitVisualizer.js format needs formatting
error components/Quantum/circuitUtils.js format needs formatting
error components/Visualization/Graphvisualization.js format needs formatting
error components/Visualization/GraphvizWrapper.js assist/source/organizeImports Sort these imports.
error components/Visualization/QuantumCircuitVis.js assist/source/organizeImports Sort these imports.
error components/Visualization/QuantumCircuitVis.js format needs formatting
error components/Visualization/ReducedVisualization.js format needs formatting
error components/Visualization/constants/VisColors.js format needs formatting
error components/Visualization/constants/VisColorsArray.js assist/source/organizeImports Sort the exported names.
error components/Visualization/constants/VisColorsArray.js format needs formatting
error components/Visualization/openqasmToQText.js format needs formatting
error components/Visualization/svgs/DynamicTableSvgReact.js format needs formatting
error components/Visualization/svgs/LaTeXGraphSvgReact.js format needs formatting
error components/Visualization/svgs/No_Viz_SVG.js assist/source/organizeImports Sort these imports.
error components/Visualization/svgs/No_Viz_SVG.js format needs formatting
… and 74 more
❌ lint — 24 errors, 41 warnings
Severity Location Rule Message
error components/Visualization/QuantumCircuitVis.js:152 react-hooks/set-state-in-effect Error: Calling setState synchronously within an effect can trigger cascading renders
error components/hooks/ProblemProvider/Problem.js:51 no-undef 'requestInfo' is not defined.
error components/hooks/ProblemProvider/Problem.js:73 react-hooks/set-state-in-effect Error: Calling setState synchronously within an effect can trigger cascading renders
error components/hooks/ProblemProvider/Reducer.js:132 react-hooks/set-state-in-effect Error: Calling setState synchronously within an effect can trigger cascading renders
error components/hooks/ProblemProvider/Reducer.js:152 react-hooks/set-state-in-effect Error: Calling setState synchronously within an effect can trigger cascading renders
error components/hooks/ProblemProvider/Reducer.js:171 react-hooks/set-state-in-effect Error: Calling setState synchronously within an effect can trigger cascading renders
error components/hooks/ProblemProvider/Reducer.js:190 react-hooks/set-state-in-effect Error: Calling setState synchronously within an effect can trigger cascading renders
error components/hooks/ProblemProvider/Reducer.js:211 react-hooks/immutability Error: Cannot access variable before it is declared
error components/hooks/ProblemProvider/Reducer.js:215 react-hooks/set-state-in-effect Error: Calling setState synchronously within an effect can trigger cascading renders
error components/hooks/ProblemProvider/Reducer.js:51 react-hooks/set-state-in-effect Error: Calling setState synchronously within an effect can trigger cascading renders
error components/hooks/ProblemProvider/Reducer.js:74 react-hooks/set-state-in-effect Error: Calling setState synchronously within an effect can trigger cascading renders
error components/hooks/ProblemProvider/Solver.js:122 react-hooks/set-state-in-effect Error: Calling setState synchronously within an effect can trigger cascading renders
error components/hooks/ProblemProvider/Solver.js:24 react-hooks/set-state-in-effect Error: Calling setState synchronously within an effect can trigger cascading renders
error components/hooks/ProblemProvider/Verifier.js:87 react-hooks/set-state-in-effect Error: Calling setState synchronously within an effect can trigger cascading renders
error components/pageblocks/ProblemRowReact.js:177 react-hooks/set-state-in-effect Error: Calling setState synchronously within an effect can trigger cascading renders
error components/pageblocks/ProblemRowReact.js:186 react-hooks/immutability Error: This value cannot be modified
error components/pageblocks/VerifyRowReact.js:52 react-hooks/set-state-in-effect Error: Calling setState synchronously within an effect can trigger cascading renders
error components/pageblocks/VerifyRowReact.js:58 react-hooks/set-state-in-effect Error: Calling setState synchronously within an effect can trigger cascading renders
error components/pageblocks/VisualizeRowReact.js:141 react-hooks/set-state-in-effect Error: Calling setState synchronously within an effect can trigger cascading renders
error components/pageblocks/VisualizeRowReact.js:145 react-hooks/set-state-in-effect Error: Calling setState synchronously within an effect can trigger cascading renders
… and 45 more

Avoids the react-hooks/set-state-in-effect lint error while keeping
the same SSR-safe "unmounted -> null" gating behavior.
sectionCardSx/innerCardSx (and similar) use transition: all for a smooth
hover effect, but background/border/shadow all differ between light and
dark variants too -- so toggling animated through every in-between color
over ~0.2-0.25s, most visibly on hover-highlighted elements. Transitions
are now forced off for one frame around the toggle itself so the swap is
instant, then re-enabled immediately after for normal hover animations.
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.

Fix dark-mode flash on page navigation; respect OS theme on first load

2 participants