Reemus Icon

How to Disable CSS Transitions When Changing ColorΒ Schemes

πŸ‘πŸ”₯β€οΈπŸ˜‚πŸ˜’πŸ˜•
views
comments

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};
};
js-icon

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();
};
js-icon

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);
};
js-icon

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);
};
js-icon

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();
};
js-icon

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);
};
js-icon

Then simply use the withoutTransition function:

const changeColorScheme = () => {
  withoutTransition(() => {
    /* Your change color scheme code... */
  });
};
js-icon

References

A huge shoutout to paco.me for the idea of using getComputedStyle

πŸ‘πŸ”₯β€οΈπŸ˜‚πŸ˜’πŸ˜•

Comments

...

Your name will be displayed publicly

Loading comments...