Skip to content

Commit 1f6034d

Browse files
Merge pull request #7533 from AnkitRewar11/fix/dark-mode-flicker-clean
fix: resolve dark mode flicker issue
2 parents 20fd5c7 + b73d447 commit 1f6034d

File tree

4 files changed

+85
-78
lines changed

4 files changed

+85
-78
lines changed

onRenderBody.js

Lines changed: 62 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,53 +5,76 @@ import lighttheme, { darktheme } from "./src/theme/app/themeStyles";
55
const themes = { light: lighttheme, dark: darktheme };
66

77
const MagicScriptTag = (props) => {
8+
// FIX: Stringify the theme object outside the template literal to prevent syntax errors caused by unescaped quotes inside theme values.
9+
const themeJSON = JSON.stringify(props.theme);
10+
11+
// Injects CSS variables and theme state strictly before the first paint to prevent FOUC.
812
const codeToRunOnClient = `
913
(function() {
10-
// 1. Keeps SYSTEM as the priority preference
11-
const themeFromLocalStorage = localStorage.getItem('${DarkThemeKey}') || '${ThemeSetting.SYSTEM}';
14+
try {
15+
// 1. Keeps SYSTEM as the priority preference
16+
const themeFromLocalStorage = localStorage.getItem('${DarkThemeKey}') || '${ThemeSetting.SYSTEM}';
1217
13-
// 2. We change the check to look for LIGHT mode explicitly
14-
const systemLightModeSetting = () => window.matchMedia ? window.matchMedia('(prefers-color-scheme: light)') : null;
15-
16-
const isLightModeActive = () => {
17-
return !!systemLightModeSetting()?.matches;
18-
};
18+
// 2. We change the check to look for LIGHT mode explicitly
19+
const systemLightModeSetting = () => window.matchMedia ? window.matchMedia('(prefers-color-scheme: light)') : null;
20+
21+
const isLightModeActive = () => {
22+
return !!systemLightModeSetting()?.matches;
23+
};
1924
20-
let colorMode;
21-
switch (themeFromLocalStorage) {
22-
case '${ThemeSetting.SYSTEM}':
23-
// LOGIC CHANGE: If Light is active -> Light. Otherwise (Dark, No Preference, or Error) -> Dark.
24-
colorMode = isLightModeActive() ? '${ThemeSetting.LIGHT}' : '${ThemeSetting.DARK}'
25-
break
26-
case '${ThemeSetting.DARK}':
27-
case '${ThemeSetting.LIGHT}':
28-
colorMode = themeFromLocalStorage
29-
break
30-
default:
31-
// 3. Fallback to DARK in case of error
32-
colorMode = '${ThemeSetting.DARK}'
33-
}
25+
let colorMode;
26+
switch (themeFromLocalStorage) {
27+
case '${ThemeSetting.SYSTEM}':
28+
// LOGIC CHANGE: If Light is active -> Light. Otherwise (Dark, No Preference, or Error) -> Dark.
29+
colorMode = isLightModeActive() ? '${ThemeSetting.LIGHT}' : '${ThemeSetting.DARK}';
30+
break;
31+
case '${ThemeSetting.DARK}':
32+
case '${ThemeSetting.LIGHT}':
33+
colorMode = themeFromLocalStorage;
34+
break;
35+
default:
36+
// 3. Fallback to DARK in case of error
37+
colorMode = '${ThemeSetting.DARK}';
38+
}
3439
35-
const root = document.documentElement;
36-
const iterate = (obj) => {
37-
if (!obj) return;
38-
Object.keys(obj).forEach(key => {
39-
if (typeof obj[key] === 'object') {
40-
iterate(obj[key])
41-
} else {
42-
root.style.setProperty("--" + key, obj[key])
40+
const root = document.documentElement;
41+
const iterate = (obj) => {
42+
if (!obj) return;
43+
Object.keys(obj).forEach(key => {
44+
if (typeof obj[key] === 'object') {
45+
iterate(obj[key]);
46+
} else {
47+
root.style.setProperty("--" + key, obj[key]);
48+
}
49+
});
50+
};
51+
52+
// FIX: Inject the JSON object directly to avoid JSON.parse breaking on nested quotes.
53+
const parsedTheme = ${themeJSON};
54+
const theme = parsedTheme[colorMode];
55+
56+
if (theme) {
57+
iterate(theme);
4358
}
44-
})
59+
60+
root.style.setProperty('--initial-color-mode', colorMode);
61+
62+
// FIX: Setting data-theme is required for global CSS styles to apply correctly before React hydration.
63+
root.setAttribute('data-theme', colorMode);
64+
65+
// Sync the calculated theme globally so ThemeManager can pick it up seamlessly.
66+
window.__theme = colorMode;
67+
68+
} catch (e) {
69+
console.error('Dark mode injection failed:', e);
4570
}
46-
const parsedTheme = JSON.parse('${JSON.stringify(props.theme)}')
47-
const theme = parsedTheme[colorMode]
48-
iterate(theme)
49-
root.style.setProperty('--initial-color-mode', colorMode);
50-
})()
71+
})();
5172
`;
5273
return <script dangerouslySetInnerHTML={{ __html: codeToRunOnClient }} />;
5374
};
5475

55-
export const onRenderBody = ( { setPreBodyComponents }) => {
56-
setPreBodyComponents(<MagicScriptTag key="theme-injection" theme={themes} />);
57-
};
76+
// FIX: Using setHeadComponents instead of setPreBodyComponents ensures the script runs
77+
// strictly in the <head>, blocking the first paint until the theme is applied and completely eliminating FOUC.
78+
export const onRenderBody = ( { setHeadComponents }) => {
79+
setHeadComponents([<MagicScriptTag key="theme-injection" theme={themes} />]);
80+
};

src/html.js

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,41 +25,20 @@ export default function HTML(props) {
2525
{props.headComponents}
2626
</head>
2727
<body {...props.bodyAttributes}>
28-
{/* Script for theme initialization - needs to run before React renders to prevent flicker */}
28+
{/* Script for banner initialization */}
2929
<script
3030
dangerouslySetInnerHTML={{
3131
__html: `
3232
(function() {
3333
try {
34-
// Theme initialization
35-
const darkThemeKey = 'theme';
36-
let initialTheme = 'system';
37-
try {
38-
initialTheme = localStorage.getItem(darkThemeKey) || 'system';
39-
} catch (e) {}
40-
41-
// Determine initial dark mode
42-
let isDarkMode = false;
43-
if (initialTheme === 'dark') {
44-
isDarkMode = true;
45-
} else if (initialTheme === 'system') {
46-
isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
47-
}
48-
49-
// Set initial color mode
50-
document.documentElement.style.setProperty(
51-
'--initial-color-mode',
52-
isDarkMode ? 'dark' : 'light'
53-
);
54-
5534
// Banner initialization
5635
var banner = sessionStorage.getItem('banner');
5736
if (banner === null)
5837
document.body.classList.add('banner1');
5938
else
6039
document.body.classList.add('banner' + banner);
6140
} catch (e) {
62-
console.error('Error in theme initialization:', e);
41+
console.error('Error in banner initialization:', e);
6342
}
6443
})();
6544
`,

src/theme/app/StyledThemeProvider.js

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@ import React, { useContext } from "react";
55
import { ThemeProvider } from "styled-components";
66
import { ThemeManagerContext } from "./ThemeManager";
77

8-
// Safe check for browser environment
9-
const isBrowser = typeof window !== "undefined";
10-
118
export const StyledThemeProvider = (props) => {
129
const { children, darkTheme, lightTheme } = props;
1310
const { isDark, didLoad } = useContext(ThemeManagerContext);
@@ -16,7 +13,7 @@ export const StyledThemeProvider = (props) => {
1613
// This ensures the server and client render the same thing initially
1714
const currentTheme = isDark ? darkTheme : lightTheme;
1815
const theme = {
19-
...(didLoad || !isBrowser ? currentTheme : transformTheme(currentTheme)),
16+
...(didLoad ? currentTheme : transformTheme(currentTheme)),
2017
};
2118

2219
return (
@@ -39,5 +36,3 @@ const transformTheme = (theme) => {
3936

4037
return newTheme;
4138
};
42-
43-

src/theme/app/ThemeManager.js

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const isBrowser = typeof window !== "undefined";
2727

2828
const systemDarkModeSetting = () =>
2929
isBrowser && window.matchMedia ? window.matchMedia("(prefers-color-scheme: dark)") : null;
30+
3031
const isDarkModeActive = () => {
3132
return !!systemDarkModeSetting()?.matches;
3233
};
@@ -36,19 +37,28 @@ const applyThemeToDOM = (theme) => {
3637
const root = window.document.documentElement;
3738
root.style.setProperty("--initial-color-mode", theme);
3839
root.setAttribute("data-theme", theme);
40+
window.__theme = theme;
3941
};
4042

4143
export const ThemeManagerProvider = (props) => {
4244
const [themeSetting, setThemeSetting] = useState(ThemeSetting.SYSTEM);
4345
const [didLoad, setDidLoad] = useState(false);
44-
const [isDark, setIsDark] = useState(false);
46+
47+
const [isDark, setIsDark] = useState(() => {
48+
if (isBrowser) {
49+
if (window.__theme === ThemeSetting.DARK) return true;
50+
if (window.__theme === ThemeSetting.LIGHT) return false;
51+
}
52+
return false;
53+
});
4554

4655
useEffect(() => {
4756
if (!isBrowser) return;
4857

4958
const root = window.document.documentElement;
50-
const initialColorValue = root.style.getPropertyValue("--initial-color-mode");
51-
59+
const initialColorValue = (root.style.getPropertyValue("--initial-color-mode") || "").trim();
60+
const actualTheme = window.__theme || initialColorValue || ThemeSetting.DARK;
61+
5262
// Get stored theme from localStorage
5363
const storedTheme = localStorage.getItem(DarkThemeKey);
5464

@@ -57,8 +67,8 @@ export const ThemeManagerProvider = (props) => {
5767
setIsDark(isDarkTheme);
5868
setThemeSetting(storedTheme);
5969
applyThemeToDOM(storedTheme);
60-
} else if (initialColorValue) {
61-
setIsDark(initialColorValue === ThemeSetting.DARK);
70+
} else if (actualTheme) {
71+
setIsDark(actualTheme === ThemeSetting.DARK);
6272
setThemeSetting(ThemeSetting.SYSTEM);
6373
} else {
6474
// Fallback to system preference
@@ -71,7 +81,7 @@ export const ThemeManagerProvider = (props) => {
7181
setDidLoad(true);
7282
}, []);
7383

74-
// Listen to system color scheme changes only when on SYSTEM mode
84+
// Listen to system color scheme changes only when on SYSTEM mode
7585
useEffect(() => {
7686
if (!isBrowser || themeSetting !== ThemeSetting.SYSTEM) return;
7787

@@ -93,11 +103,11 @@ export const ThemeManagerProvider = (props) => {
93103
const newIsDark = !isDark;
94104
const newTheme = newIsDark ? ThemeSetting.DARK : ThemeSetting.LIGHT;
95105

96-
// Update state
106+
// Update state
97107
setIsDark(newIsDark);
98108
setThemeSetting(newTheme);
99109

100-
// Apply to DOM immediately
110+
// Apply to DOM immediately
101111
applyThemeToDOM(newTheme);
102112

103113
// Persist to localStorage
@@ -129,14 +139,14 @@ export const ThemeManagerProvider = (props) => {
129139
return;
130140
}
131141

132-
// Update state
142+
// Update state
133143
setIsDark(newIsDark);
134144
setThemeSetting(setting);
135145

136146
// Apply to DOM immediately
137147
applyThemeToDOM(themeToApply);
138148

139-
// Persist to localStorage
149+
// Persist to localStorage
140150
localStorage.setItem(DarkThemeKey, setting);
141151
},
142152
[isDark]

0 commit comments

Comments
 (0)