Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/frontend/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import Navbar from "./components/Navbar";
import { absoluteUrl, defaultDescription, siteName, siteUrl } from "./seo";
import "./globals.css";

const THEME_INLINE_SCRIPT = `(function(){try{var e=window.localStorage.getItem("stellar-bounty-theme");if(e==="light"||e==="dark"){document.documentElement.classList.toggle("dark",e==="dark");document.documentElement.style.colorScheme=e;return}}catch(e){}var n=window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";document.documentElement.classList.toggle("dark",n==="dark");document.documentElement.style.colorScheme=n})();`;

export const metadata: Metadata = {
metadataBase: siteUrl,
title: {
Expand Down Expand Up @@ -33,6 +35,9 @@ export const metadata: Metadata = {
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<script dangerouslySetInnerHTML={{ __html: THEME_INLINE_SCRIPT }} />
</head>
<body className="bg-slate-50 text-slate-950 transition-colors dark:bg-slate-950 dark:text-slate-100">
<ThemeProvider>
<WalletProvider>
Expand Down
24 changes: 19 additions & 5 deletions apps/frontend/components/ThemeProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,25 @@ type ThemeContextValue = {
const STORAGE_KEY = "stellar-bounty-theme";
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);

function getInitialTheme(): Theme {
/**
* Read the saved theme from localStorage, falling back to the system
* preference, then to `"dark"`. This function is safe to call during
* both SSR and hydration.
*
* The same logic is also injected as an inline `<script>` in the HTML
* `<head>` (see `app/layout.tsx`) so that the correct theme class is
* present before React hydrates — eliminating the flash of incorrect
* theme (FOUC).
*/
export function initializeTheme(): Theme {
if (typeof window === "undefined") return "dark";

const storedTheme = window.localStorage.getItem(STORAGE_KEY);
if (storedTheme === "light" || storedTheme === "dark") return storedTheme;
try {
const stored = window.localStorage.getItem(STORAGE_KEY);
if (stored === "light" || stored === "dark") return stored;
} catch {
// localStorage may be blocked in some environments
}

return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
Expand All @@ -30,7 +44,7 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>("dark");

useEffect(() => {
const initialTheme = getInitialTheme();
const initialTheme = initializeTheme();
setTheme(initialTheme);
applyTheme(initialTheme);
}, []);
Expand All @@ -57,4 +71,4 @@ export function useTheme() {
}

return context;
}
}
Loading