How to Disable CSS Transitions When Changing ColorΒ Schemes
Does your website flicker or flash weirdly when toggling between light and dark modes? This can be mitigated by implementing proper transitions during changes in color schemes, or alternatively, by temporarily disabling CSS transitions while the change is in effect. Let's explore how to do the latter.
Quick note: though many would call it light and dark mode, the official CSS term is 'color scheme', hence I will use that term throughout this article
How to disable CSS transitions temporarily
To begin, we'll implement a function that enables and disables CSS transitions.
const transitionManager = () => {
// Create HTML style element with CSS selector that targets all
// elements and applies CSS to disable transitions
const style = document.createElement("style");
const css = document.createTextNode(`* {
-webkit-transition: none !important;
-moz-transition: none !important;
-o-transition: none !important;
-ms-transition: none !important;
transition: none !important;
}`);
style.appendChild(css);
// Create functions for adding and remove the style element from
// the page <head> tag
const enable = () => document.head.removeChild(style);
const disable = () => document.head.appendChild(style);
return {enable, disable, style};
};
Change color scheme without transitions
Now that we have control over enabling and disabling CSS transitions, we can utilize this when changing color schemes. However, merely disabling transitions, changing the color scheme, and then enabling transitions will not work. Meaning the following code will not work:
// Incorrect implementation - this will not work!
const changeColorScheme = () => {
const transitions = transitionManager();
transitions.disable();
/* Your change color scheme code... */
transitions.enable();
};
I assume (correct me if I'm wrong) the reason this does not work is due to the browser not registering the transition-disabling CSS before the color scheme change code is invoked. This probably has something to do with:
- The browser batching DOM and style updates to optimize performance
- Similar batching processes implemented in your UI framework
So, how can we overcome this issue?
Worst solution - setTimeout
My initial attempt had me trying something like this:
const changeColorScheme = () => {
const transitions = transitionManager();
transitions.disable();
setTimeout(() => {
/* Your change color scheme code... */
setTimeout(() => transitions.enable(), 150);
}, 150);
};
This can work sometimes, but it is very unreliable. Why exactly, I'm not sure. I had mixed results no matter how much I increased the timeout. There has to be a better way right?
Better solution - requestAnimationFrame
This method provides more reliable results compared to using setTimeout
. The browser exposes an API via window.requestAnimationFrame which allows us to schedule a function to run before the next UI repaint. This ensures the code execution before any transitions are visually rendered.
Here's how you can implement this method:
const changeColorScheme = () => {
const transitions = transitionManager();
transitions.disable();
/* Your change color scheme code... */
window.requestAnimationFrame(transitions.enable);
};
Not only is the code easier to understand, but it also works better! However, there is still a better way than this.
Best solution - getComputedStyle
Thankfully, there is an API exposed by browsers to get the exact behaviour needed. The window.getComputedStyle API is designed to extract the computed CSS properties of an element. In doing this, it will apply all the styles necessary to get the result it needs which in turn forces the browser to 'repaint' any CSS changes.
To implement this method, use the following code:
const changeColorScheme = () => {
const transitions = transitionManager();
transitions.disable();
/* Your change color scheme code... */
// Request the computed CSS of the style element we created
// forcing the browser to evaluate and paint it which in turn disables transitions
// You need to access any property of the result like `opacity` for it to work
window.getComputedStyle(transitions.style).opacity;
transitions.enable();
};
A genius method that is simple and works perfectly!
Ultimate solution for changing color scheme without transitions
Yes I gave you the best method I can find above, but there is still a problem. Not every browser supports window.getComputedStyle
or window.requestAnimationFrame
.
That's why I wrote a handy function that checks what the browser supports and uses the best solution available! This function can be used for any situation where you need to disable transitions temporarily.
let timeoutAction;
let timeoutEnable;
// Perform a task without any css transitions
export const withoutTransition = (action: () => any) => {
// Clear fallback timeouts
clearTimeout(timeoutAction);
clearTimeout(timeoutEnable);
// Create style element to disable transitions
const style = document.createElement("style");
const css = document.createTextNode(`* {
-webkit-transition: none !important;
-moz-transition: none !important;
-o-transition: none !important;
-ms-transition: none !important;
transition: none !important;
}`);
style.appendChild(css);
// Functions to insert and remove style element
const disable = () => document.head.appendChild(style);
const enable = () => document.head.removeChild(style);
// Best method, getComputedStyle forces browser to repaint
if (typeof window.getComputedStyle !== "undefined") {
disable();
action();
window.getComputedStyle(style).opacity;
enable();
return;
}
// Better method, requestAnimationFrame processes function before next repaint
if (typeof window.requestAnimationFrame !== "undefined") {
disable();
action();
window.requestAnimationFrame(enable);
return;
}
// Fallback
disable();
timeoutAction = setTimeout(() => {
action();
timeoutEnable = setTimeout(enable, 120);
}, 120);
};
Then simply use the withoutTransition
function:
const changeColorScheme = () => {
withoutTransition(() => {
/* Your change color scheme code... */
});
};
References
A huge shoutout to paco.me for the idea of using getComputedStyle
Comments