Устранение FOUC-эффекта при использовании тёмной темы сайта в Next.js

9 сентября 2025 г.

Что такое FOUC-эффект

FOUC-эффект (Flash of Unstyled Content) – это явление, когда веб-страница отображается в стилях по умолчанию, до загрузки внешнего файла CSS или JavaScript.

Эффект чаще всего проявляется при использовании тёмной темы – сначала применяется тема по умолчанию (обычно светлая), затем происходит переход в тёмный режим, из-за чего интерфейс на мгновение мерцает.

Решение

  1. В корневом макете сайта устанавливаем тему по умолчанию посредством атрибута data-theme тега html.
app/layout.js
import "./globals.css";
 
export default function RootLayout({ children }) {
  return (
    <html lang="ru">
    <html lang="ru" data-theme="light">
      <body className={`${font.className} antialiased`}>
        {children}
      </body>
    </html>
  );
}
  1. В дальнейшем, все стили тёмной и светлой тем будут зависеть от текущего значения этого атрибута. Например, при использовании библиотеки Tailwind CSS, можно задать глобальный модификатор стилей dark: при помощи директивы @custom-variant:
app/globals.css
@import "tailwindcss";
 
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
  1. Создаём скрипт, который будет изменять атрибут data-theme тега html во время загрузки сайта, в зависимости от значения, сохранённого в localStorage:
app/utils/theme.js
const script = `
  const theme = localStorage.theme;
  if (theme) document.querySelector("html").dataset.theme = theme;
`;
 
export default function Theme() {
  return <script dangerouslySetInnerHTML={{ __html: script }}></script>;
}
  1. Добавляем подготовленный скрипт в корневой макет сайта сразу после тега body:
app/layout.js
import "./globals.css";
 
import Theme from "@/app/utils/theme";
 
export default function RootLayout({ children }) {
  return (
    <html lang="ru" data-theme="light">
    <html lang="ru" data-theme="light" suppressHydrationWarning>
      <body className={`${font.className} antialiased`}>
        <Theme />
        {children}
      </body>
    </html>
  );
}

Здесь важно добавить к тегу html атрибут suppressHydrationWarning, чтобы подавить предупреждение React Hydration Error.

  1. Нестилизованный пример переключателя тёмной/светлой темы с сохранением в localStorage:
app/ui/toggle.js
"use client";
 
import { useRef, useEffect } from "react";
 
export default function Toggle() {
  const checkbox = useRef(null);
 
  const toggleTheme = () => {
    const theme = checkbox.current.checked ? "dark" : "light";
    document.querySelector("html").dataset.theme = theme;
    localStorage.theme = theme;
  };
 
  useEffect(() => {
    const theme = document.querySelector("html").dataset.theme;
    if (theme === "dark") checkbox.current.checked = true;
  }, []);
 
  return (
    <label aria-label="Тема">
      <input ref={checkbox} type="checkbox" onChange={toggleTheme} />
      <span>Тёмная тема</span>
    </label>
  );
}

Переключатель можно вставить в любое место сайта. Обычно его располагают в правом верхнем углу.

Теперь, при загрузке сайта, он будет сразу открываться в той теме, которая сохранена в localStorage, без FOUC-эффекта.