Skip to content
101 changes: 62 additions & 39 deletions onRenderBody.js
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why was the existing approach completely ripped out and replaced?

What is precisely wrong with the current approach? Fix that.

If the current approach is completely offbase - ok - explain that and why a complete rewrite is needed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@AnkitRewar11 can you answer this?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Hey @saurabhraghuvanshii, just wanted to clearly explain why the master branch approach was failing and how this PR fixes it.

I’ve only made a few focused fixes to solve the dark mode flicker issue:

Wrong Injection Placement: The script was running too late (inside ), so the page first showed light mode. I moved it to so the correct theme loads before anything is displayed.

Missing data-theme: The code wasn’t setting the data-theme attribute on the HTML root, which is needed for dark mode styles. I’ve added that.

SSR / Hydration Mismatch: The server knew the theme, but the frontend didn’t, so it defaulted to light mode and caused flicker. I fixed this by passing the theme (window.__theme) so both stay in sync.

JSON Parsing Issue: The previous way of parsing theme data could break if there were quotes in the string. I made it safer to avoid crashes.

Now the changes are minimal, clean, and focused and the flicker issue is completely resolved. I hope all your questions have been answered. If there’s anything else you’d like to know, please let me know.

Original file line number Diff line number Diff line change
Expand Up @@ -5,53 +5,76 @@ import lighttheme, { darktheme } from "./src/theme/app/themeStyles";
const themes = { light: lighttheme, dark: darktheme };

const MagicScriptTag = (props) => {
// FIX: Stringify the theme object outside the template literal to prevent syntax errors caused by unescaped quotes inside theme values.
const themeJSON = JSON.stringify(props.theme);

// Injects CSS variables and theme state strictly before the first paint to prevent FOUC.
const codeToRunOnClient = `
(function() {
// 1. Keeps SYSTEM as the priority preference
const themeFromLocalStorage = localStorage.getItem('${DarkThemeKey}') || '${ThemeSetting.SYSTEM}';
try {
// 1. Keeps SYSTEM as the priority preference
const themeFromLocalStorage = localStorage.getItem('${DarkThemeKey}') || '${ThemeSetting.SYSTEM}';

// 2. We change the check to look for LIGHT mode explicitly
const systemLightModeSetting = () => window.matchMedia ? window.matchMedia('(prefers-color-scheme: light)') : null;

const isLightModeActive = () => {
return !!systemLightModeSetting()?.matches;
};
// 2. We change the check to look for LIGHT mode explicitly
const systemLightModeSetting = () => window.matchMedia ? window.matchMedia('(prefers-color-scheme: light)') : null;
const isLightModeActive = () => {
return !!systemLightModeSetting()?.matches;
};

let colorMode;
switch (themeFromLocalStorage) {
case '${ThemeSetting.SYSTEM}':
// LOGIC CHANGE: If Light is active -> Light. Otherwise (Dark, No Preference, or Error) -> Dark.
colorMode = isLightModeActive() ? '${ThemeSetting.LIGHT}' : '${ThemeSetting.DARK}'
break
case '${ThemeSetting.DARK}':
case '${ThemeSetting.LIGHT}':
colorMode = themeFromLocalStorage
break
default:
// 3. Fallback to DARK in case of error
colorMode = '${ThemeSetting.DARK}'
}
let colorMode;
switch (themeFromLocalStorage) {
case '${ThemeSetting.SYSTEM}':
// LOGIC CHANGE: If Light is active -> Light. Otherwise (Dark, No Preference, or Error) -> Dark.
colorMode = isLightModeActive() ? '${ThemeSetting.LIGHT}' : '${ThemeSetting.DARK}';
break;
case '${ThemeSetting.DARK}':
case '${ThemeSetting.LIGHT}':
colorMode = themeFromLocalStorage;
break;
default:
// 3. Fallback to DARK in case of error
colorMode = '${ThemeSetting.DARK}';
}

const root = document.documentElement;
const iterate = (obj) => {
if (!obj) return;
Object.keys(obj).forEach(key => {
if (typeof obj[key] === 'object') {
iterate(obj[key])
} else {
root.style.setProperty("--" + key, obj[key])
const root = document.documentElement;
const iterate = (obj) => {
if (!obj) return;
Object.keys(obj).forEach(key => {
if (typeof obj[key] === 'object') {
iterate(obj[key]);
} else {
root.style.setProperty("--" + key, obj[key]);
}
});
};

// FIX: Inject the JSON object directly to avoid JSON.parse breaking on nested quotes.
const parsedTheme = ${themeJSON};
const theme = parsedTheme[colorMode];

if (theme) {
iterate(theme);
}
})

root.style.setProperty('--initial-color-mode', colorMode);

// FIX: Setting data-theme is required for global CSS styles to apply correctly before React hydration.
root.setAttribute('data-theme', colorMode);

// Sync the calculated theme globally so ThemeManager can pick it up seamlessly.
window.__theme = colorMode;

} catch (e) {
console.error('Dark mode injection failed:', e);
}
const parsedTheme = JSON.parse('${JSON.stringify(props.theme)}')
const theme = parsedTheme[colorMode]
iterate(theme)
root.style.setProperty('--initial-color-mode', colorMode);
})()
})();
`;
return <script dangerouslySetInnerHTML={{ __html: codeToRunOnClient }} />;
};

export const onRenderBody = ( { setPreBodyComponents }) => {
setPreBodyComponents(<MagicScriptTag key="theme-injection" theme={themes} />);
};
// FIX: Using setHeadComponents instead of setPreBodyComponents ensures the script runs
// strictly in the <head>, blocking the first paint until the theme is applied and completely eliminating FOUC.
export const onRenderBody = ( { setHeadComponents }) => {
setHeadComponents([<MagicScriptTag key="theme-injection" theme={themes} />]);
};
25 changes: 2 additions & 23 deletions src/html.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,41 +25,20 @@ export default function HTML(props) {
{props.headComponents}
</head>
<body {...props.bodyAttributes}>
{/* Script for theme initialization - needs to run before React renders to prevent flicker */}
{/* Script for banner initialization */}
<script
dangerouslySetInnerHTML={{
__html: `
(function() {
try {
// Theme initialization
const darkThemeKey = 'theme';
let initialTheme = 'system';
try {
initialTheme = localStorage.getItem(darkThemeKey) || 'system';
} catch (e) {}

// Determine initial dark mode
let isDarkMode = false;
if (initialTheme === 'dark') {
isDarkMode = true;
} else if (initialTheme === 'system') {
isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
}

// Set initial color mode
document.documentElement.style.setProperty(
'--initial-color-mode',
isDarkMode ? 'dark' : 'light'
);

// Banner initialization
var banner = sessionStorage.getItem('banner');
if (banner === null)
document.body.classList.add('banner1');
else
document.body.classList.add('banner' + banner);
} catch (e) {
console.error('Error in theme initialization:', e);
console.error('Error in banner initialization:', e);
}
})();
`,
Expand Down
7 changes: 1 addition & 6 deletions src/theme/app/StyledThemeProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ import React, { useContext } from "react";
import { ThemeProvider } from "styled-components";
import { ThemeManagerContext } from "./ThemeManager";

// Safe check for browser environment
const isBrowser = typeof window !== "undefined";

export const StyledThemeProvider = (props) => {
const { children, darkTheme, lightTheme } = props;
const { isDark, didLoad } = useContext(ThemeManagerContext);
Expand All @@ -16,7 +13,7 @@ export const StyledThemeProvider = (props) => {
// This ensures the server and client render the same thing initially
const currentTheme = isDark ? darkTheme : lightTheme;
const theme = {
...(didLoad || !isBrowser ? currentTheme : transformTheme(currentTheme)),
...(didLoad ? currentTheme : transformTheme(currentTheme)),
};

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

return newTheme;
};


30 changes: 20 additions & 10 deletions src/theme/app/ThemeManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const isBrowser = typeof window !== "undefined";

const systemDarkModeSetting = () =>
isBrowser && window.matchMedia ? window.matchMedia("(prefers-color-scheme: dark)") : null;

const isDarkModeActive = () => {
return !!systemDarkModeSetting()?.matches;
};
Expand All @@ -36,19 +37,28 @@ const applyThemeToDOM = (theme) => {
const root = window.document.documentElement;
root.style.setProperty("--initial-color-mode", theme);
root.setAttribute("data-theme", theme);
window.__theme = theme;
};

export const ThemeManagerProvider = (props) => {
const [themeSetting, setThemeSetting] = useState(ThemeSetting.SYSTEM);
const [didLoad, setDidLoad] = useState(false);
const [isDark, setIsDark] = useState(false);

const [isDark, setIsDark] = useState(() => {
if (isBrowser) {
if (window.__theme === ThemeSetting.DARK) return true;
if (window.__theme === ThemeSetting.LIGHT) return false;
}
return false;
});

useEffect(() => {
if (!isBrowser) return;

const root = window.document.documentElement;
const initialColorValue = root.style.getPropertyValue("--initial-color-mode");

const initialColorValue = (root.style.getPropertyValue("--initial-color-mode") || "").trim();
const actualTheme = window.__theme || initialColorValue || ThemeSetting.DARK;

// Get stored theme from localStorage
Copy link
Copy Markdown
Member

@rishiraj38 rishiraj38 Mar 28, 2026

Choose a reason for hiding this comment

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

Please don’t remove the comments.

Copy link
Copy Markdown
Author

@AnkitRewar11 AnkitRewar11 Mar 31, 2026

Choose a reason for hiding this comment

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

Hi @rishiraj38, I hope everything got added properly. I’ve added that one comment as you suggested.

const storedTheme = localStorage.getItem(DarkThemeKey);

Expand All @@ -57,8 +67,8 @@ export const ThemeManagerProvider = (props) => {
setIsDark(isDarkTheme);
setThemeSetting(storedTheme);
applyThemeToDOM(storedTheme);
} else if (initialColorValue) {
setIsDark(initialColorValue === ThemeSetting.DARK);
} else if (actualTheme) {
setIsDark(actualTheme === ThemeSetting.DARK);
setThemeSetting(ThemeSetting.SYSTEM);
} else {
// Fallback to system preference
Expand All @@ -71,7 +81,7 @@ export const ThemeManagerProvider = (props) => {
setDidLoad(true);
}, []);

// Listen to system color scheme changes only when on SYSTEM mode
// Listen to system color scheme changes only when on SYSTEM mode
useEffect(() => {
if (!isBrowser || themeSetting !== ThemeSetting.SYSTEM) return;

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

// Update state
// Update state
setIsDark(newIsDark);
setThemeSetting(newTheme);

// Apply to DOM immediately
// Apply to DOM immediately
applyThemeToDOM(newTheme);

// Persist to localStorage
Expand Down Expand Up @@ -129,14 +139,14 @@ export const ThemeManagerProvider = (props) => {
return;
}

// Update state
// Update state
setIsDark(newIsDark);
setThemeSetting(setting);

// Apply to DOM immediately
applyThemeToDOM(themeToApply);

// Persist to localStorage
// Persist to localStorage
localStorage.setItem(DarkThemeKey, setting);
},
[isDark]
Expand Down
Loading