Merge pull request #42 from phranck/fix/theme-flash

Fix: Eliminate theme flash (FOUC) on page reload
This commit is contained in:
phranck
2026-01-31 17:09:22 +01:00
3 changed files with 35 additions and 15 deletions
+13 -12
View File
@@ -4,7 +4,6 @@ import {
createContext,
useCallback,
useContext,
useEffect,
useState,
type ReactNode,
} from "react";
@@ -25,19 +24,21 @@ const ThemeContext = createContext<ThemeContextValue>({
const STORAGE_KEY = "tuikit-theme";
/**
* Reads the theme that the blocking script already applied to the DOM.
* Falls back to "amber" if nothing is set yet (SSR or first visit).
*/
function getInitialTheme(): Theme {
if (typeof document !== "undefined") {
const attr = document.documentElement.getAttribute("data-theme") as Theme | null;
if (attr && themes.includes(attr)) return attr;
}
return "amber";
}
/** Provides theme state and applies it to the document root via data-theme attribute. */
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState<Theme>("amber");
useEffect(() => {
const stored = localStorage.getItem(STORAGE_KEY) as Theme | null;
if (stored && themes.includes(stored)) {
setThemeState(stored);
document.documentElement.setAttribute("data-theme", stored);
} else {
document.documentElement.setAttribute("data-theme", "amber");
}
}, []);
const [theme, setThemeState] = useState<Theme>(getInitialTheme);
const setTheme = useCallback((next: Theme) => {
setThemeState(next);
+20 -1
View File
@@ -31,8 +31,27 @@ export default function RootLayout({
}: Readonly<{
children: React.ReactNode;
}>) {
/** Inline script that runs before first paint to prevent theme flash (FOUC). */
const themeInitScript = `
(function() {
try {
var stored = localStorage.getItem('tuikit-theme');
if (stored && ['green','amber','white','red'].includes(stored)) {
document.documentElement.setAttribute('data-theme', stored);
} else {
document.documentElement.setAttribute('data-theme', 'amber');
}
} catch(e) {
document.documentElement.setAttribute('data-theme', 'amber');
}
})();
`;
return (
<html lang="en" className="dark">
<html lang="en" className="dark" suppressHydrationWarning>
<head>
<script dangerouslySetInnerHTML={{ __html: themeInitScript }} />
</head>
<body
className={`${geistMono.variable} antialiased`}
>
+2 -2
View File
@@ -253,7 +253,7 @@ export default function Home() {
{/* Footer */}
<footer className="border-t border-border bg-card/30 backdrop-blur-sm">
<div className="mx-auto flex max-w-6xl flex-col items-center gap-2 px-6 py-8 text-center">
<div className="mx-auto flex max-w-6xl flex-col items-center gap-0.5 px-6 py-8 text-center">
<span className="text-base text-muted">
Made with ❤️ in Bregenz
</span>
@@ -267,7 +267,7 @@ export default function Home() {
href="https://creativecommons.org/licenses/by-nc-sa/4.0/"
target="_blank"
rel="noopener noreferrer"
className="mt-2 text-xs text-muted/70 transition-colors hover:text-foreground"
className="mt-6 text-xs text-muted/70 transition-colors hover:text-foreground"
>
CC BY-NC-SA 4.0
</a>