fix(ui): prevent flash of light mode on dark-mode page loads

Adds a synchronous inline script in the root layout that applies the
`dark` class to <html> before React hydrates, and gates ThemeContext's
DOM-write effect on a `hydrated` flag so the initial `isDarkMode=false`
state doesn't briefly undo the class the script set.

https://claude.ai/code/session_01SkckoL68328QgB5ifQwntT
This commit is contained in:
Claude 2026-05-19 17:01:21 +00:00
parent cf059b94cc
commit 2d573cfc34
No known key found for this signature in database
2 changed files with 24 additions and 3 deletions

View file

@ -13,6 +13,20 @@ export const metadata: Metadata = {
icons: { icon: "./favicon.ico" },
};
// Sets the `dark` class on <html> synchronously before React hydrates, so
// returning dark-mode users don't see a flash of light theme on page load.
const darkModeInitScript = `
(function () {
try {
var stored = window.localStorage.getItem('litellm-dark-mode');
if (stored === 'true') {
document.documentElement.classList.add('dark');
document.documentElement.style.colorScheme = 'dark';
}
} catch (e) {}
})();
`;
export default function RootLayout({
children,
}: Readonly<{
@ -20,6 +34,9 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<head>
<script dangerouslySetInnerHTML={{ __html: darkModeInitScript }} />
</head>
<body className={inter.className}>
<ReactQueryProvider>
<AntdGlobalProvider>{children}</AntdGlobalProvider>

View file

@ -44,14 +44,18 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children, accessTo
const [logoUrl, setLogoUrl] = useState<string | null>(null);
const [faviconUrl, setFaviconUrl] = useState<string | null>(null);
const [isDarkMode, setIsDarkModeState] = useState<boolean>(false);
const [hydrated, setHydrated] = useState(false);
// Hydrate dark mode from localStorage after mount to avoid SSR mismatch.
// Hydrate state from the value the inline init script already applied
// (see `darkModeInitScript` in app/layout.tsx). Until this runs we leave
// the DOM untouched so the SSR'd class set by the script survives.
useEffect(() => {
setIsDarkModeState(readInitialDarkMode());
setHydrated(true);
}, []);
useEffect(() => {
if (typeof document === "undefined") return;
if (!hydrated || typeof document === "undefined") return;
const root = document.documentElement;
if (isDarkMode) {
root.classList.add("dark");
@ -65,7 +69,7 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children, accessTo
} catch {
// ignore localStorage write errors
}
}, [isDarkMode]);
}, [isDarkMode, hydrated]);
const setIsDarkMode = useCallback((value: boolean) => {
setIsDarkModeState(value);