diff --git a/src/app/globals.css b/src/app/globals.css index 05b35fb..fd58d32 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -140,6 +140,51 @@ body { -webkit-font-smoothing: antialiased; } +/* + * Apply the user's stored theme preference on first paint using only CSS. + * `:root.dark` is set by ThemeSync after mount; the explicit `:root.dark` + * selector below the `@media` block wins over the media query when the + * user has made a stored choice (applied before paint by the ThemeSync + * effect running synchronously after hydration). + * + * This file deliberately does not contain any - ); + // Intentionally empty: theme is applied client-side via ThemeSync. + return null; } \ No newline at end of file diff --git a/src/components/theme/theme-sync.tsx b/src/components/theme/theme-sync.tsx new file mode 100644 index 0000000..d98492a --- /dev/null +++ b/src/components/theme/theme-sync.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { useEffect } from "react"; + +/** + * Applies the user's stored theme preference as soon as React mounts. + * + * The first paint already uses the OS preference (handled in globals.css via + * `@media (prefers-color-scheme: dark)`), so this is purely the override for + * the rare user who picked the opposite of their OS default. The brief flash + * is intentional — it avoids the only path Next.js 16 + React 19 leave for + * a pre-paint inline script: a `