Skip to content
Open
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
26 changes: 26 additions & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ import "@/utils/earlyErrorSuppression";
import { ClientProviders } from "@/components/ClientProviders";
import { headers } from "next/headers";

/**
* Synchronous blocking script that runs before React hydrates.
* It reads the persisted theme from localStorage (matching the
* `storageKey="theme"` configured on next-themes) and applies (or
* removes) the `dark` class on the <html> element so the first paint
* already matches the user's preferred theme. Without this script,
* dark-mode users see a flash of light content (FOUC) on hard reload
* because next-themes' class attribute is only applied after hydration.
*
* CLS = 0 because the classList mutation is non-geometric and React
* reconciles the className diff silently thanks to `suppressHydrationWarning`.
*/
const themeBootstrapScript = `(function(){try{var t=localStorage.getItem('theme');var d=t==='dark'||((t===null||t==='system')&&window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches);var c=document.documentElement.classList;c.remove('light','dark');if(d){c.add('dark');}else if(t==='light'){c.add('light');}document.documentElement.style.colorScheme=d?'dark':'light';}catch(e){}})();`;

const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
Expand Down Expand Up @@ -39,6 +53,18 @@ export default async function RootLayout({
dir={isRTL ? "rtl" : "ltr"}
suppressHydrationWarning
>
<head>
{/*
dangerouslySetInnerHTML is used because this script must run
synchronously before paint. It is a static string we control,
so there is no XSS risk. `suppressHydrationWarning` on <html>
absorbs the className diff that next-themes writes after hydration.
*/}
<script
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: themeBootstrapScript }}
/>
</head>
<body
className={`${geistSans.variable} ${geistMono.variable} bg-background font-sans text-foreground antialiased`}
>
Expand Down