August 22, 2026 · 3 min read
Why half the site kept its old colours when the theme changed
The site needed two themes: light by default, dark on a toggle. Ordinary setup — CSS variables on the html element, a data-theme="dark" attribute swapping their values.
The toggle worked immediately. Background changed, borders changed, buttons changed. And the project card headings stayed near-black — on a black background. Reloading the page fixed everything.
The first guess was caching, or React not repainting. Both wrong: the markup never changed, only the variable values did.
The real cause is elsewhere. Those headings carried transition-colors for a smooth hover. When a normal property changes, the browser plays the transition honestly. But here it was not the property that changed — it was the variable the property reads from. For an unregistered custom property, Chrome does not treat that as a reason to recompute the animated property: the colour stays until something else touches the element.
It is easy to confirm. Switch the theme and read the computed colour: the variable is already new, the element's colour is not.
The fix is to drop transitions for the duration of the switch. The class lives for exactly one style recalculation. The key line is void root.offsetHeight — reading a size forces the browser to recompute styles right there, while transitions are still off. The new colours are committed as the starting state, so removing the class afterwards animates nothing.
Worth saying why not requestAnimationFrame. The first version removed the class on the next frame, which looked more correct. But a background tab draws no frames: the callback would never fire, the class would stick forever, and every animation on the site would die with it. A forced reflow always works.
The side benefit was pleasant: the theme now switches instantly instead of spreading out into half a second of each element repainting separately.
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);}