К содержимому
Hub
Все заметки

22 августа 2026 г. · 3 мин чтения

Почему при смене темы половина сайта осталась старого цвета

CSSChromeТёмная темаОтладка

Сайту понадобились две темы: светлая по умолчанию и тёмная по кнопке. Устройство обычное — переменные CSS на элементе html, признак data-theme="dark" переключает их значения.

Переключатель заработал сразу. Фон менялся, границы менялись, кнопки менялись. А заголовки карточек проектов оставались почти чёрными — на чёрном фоне. Перезагрузка страницы всё чинила.

Первая мысль была про кэш и про то, что React не перерисовал. Обе неверные: разметка не менялась вообще, менялись только значения переменных.

Настоящая причина в другом. У заголовков стоял transition-colors — плавная смена цвета при наведении. Когда меняется обычное свойство, браузер честно проигрывает переход. Но здесь менялось не свойство, а переменная, из которой оно берётся. Для непривязанной пользовательской переменной Chrome не считает это поводом пересчитать анимируемое свойство: цвет остаётся прежним до тех пор, пока элемент не тронут чем-то ещё.

Проверить это просто. Переключите тему и посмотрите вычисленный цвет: переменная уже новая, а color у элемента — старый.

Решение — снять переходы на время смены:

Класс висит ровно один пересчёт стилей. Ключевая строка здесь void root.offsetHeight: чтение размера заставляет браузер пересчитать стили немедленно, пока переходы ещё выключены. Новые цвета закрепляются как исходное состояние, и снятие класса ниже уже ничего не анимирует.

Отдельно стоит сказать, почему не requestAnimationFrame. Первая версия снимала класс в следующем кадре — и это выглядело правильнее. Но в фоновой вкладке кадры не рисуются: обработчик не вызвался бы, класс завис бы навсегда, и вместе с ним умерли бы все анимации сайта. Принудительный пересчёт работает всегда.

Побочная выгода оказалась приятной: тема теперь переключается мгновенно, а не расползается полусекундным перекрашиванием каждого элемента по отдельности.

React · TSX
function toggle() {  const next = theme === "dark" ? "light" : "dark";  const root = document.documentElement;  root.classList.add("theme-switching");  if (next === "dark") {    root.setAttribute("data-theme", "dark");  } else {    root.removeAttribute("data-theme");  }  // Чтение offsetHeight заставляет пересчитать стили прямо здесь,  // пока переходы выключены. Именно так, а не через  // requestAnimationFrame: в фоновой вкладке кадры не рисуются,  // обработчик бы не вызвался, и класс завис бы навсегда.  void root.offsetHeight;  root.classList.remove("theme-switching");  localStorage.setItem("ithub:theme", next);  setTheme(next);}