22 августа 2026 г. · 3 мин чтения
Почему при смене темы половина сайта осталась старого цвета
Сайту понадобились две темы: светлая по умолчанию и тёмная по кнопке. Устройство обычное — переменные CSS на элементе html, признак data-theme="dark" переключает их значения.
Переключатель заработал сразу. Фон менялся, границы менялись, кнопки менялись. А заголовки карточек проектов оставались почти чёрными — на чёрном фоне. Перезагрузка страницы всё чинила.
Первая мысль была про кэш и про то, что React не перерисовал. Обе неверные: разметка не менялась вообще, менялись только значения переменных.
Настоящая причина в другом. У заголовков стоял transition-colors — плавная смена цвета при наведении. Когда меняется обычное свойство, браузер честно проигрывает переход. Но здесь менялось не свойство, а переменная, из которой оно берётся. Для непривязанной пользовательской переменной Chrome не считает это поводом пересчитать анимируемое свойство: цвет остаётся прежним до тех пор, пока элемент не тронут чем-то ещё.
Проверить это просто. Переключите тему и посмотрите вычисленный цвет: переменная уже новая, а color у элемента — старый.
Решение — снять переходы на время смены:
Класс висит ровно один пересчёт стилей. Ключевая строка здесь void root.offsetHeight: чтение размера заставляет браузер пересчитать стили немедленно, пока переходы ещё выключены. Новые цвета закрепляются как исходное состояние, и снятие класса ниже уже ничего не анимирует.
Отдельно стоит сказать, почему не requestAnimationFrame. Первая версия снимала класс в следующем кадре — и это выглядело правильнее. Но в фоновой вкладке кадры не рисуются: обработчик не вызвался бы, класс завис бы навсегда, и вместе с ним умерли бы все анимации сайта. Принудительный пересчёт работает всегда.
Побочная выгода оказалась приятной: тема теперь переключается мгновенно, а не расползается полусекундным перекрашиванием каждого элемента по отдельности.
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);}