Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pause/resume CSS animations when switching tabs

I have a bunch of long-running CSS animations on a page. I want to pause them when a user switches to another tab and resume them when the user is back to the original tab again. For the sake of simplicity I don't aim for a cross-browser solution at this point; making it work in Chrome should be enough.

document.addEventListener("visibilitychange", function() {
  if (document.hidden) {
    document.querySelector("#test").classList.add("paused");
  } else {
    document.querySelector("#test").classList.remove("paused");    
  }
});
div#test {
  width: 50px;
  height: 50px;
  background-color: red;
  position: absolute;
  left: 10vw;
  animation-name: move;
  animation-duration: 5s;
  animation-fill-mode: forwards;
  animation-timing-function: linear;
}

@keyframes move {
  to {
    left: 90vw
  }
}

.paused {
  animation-play-state: paused !important;
  -webkit-animation-play-state: paused !important;
  -moz-animation-play-state: paused !important;
}
<div id="test"></div>

The code above moves a red rectangle horizontally. It takes 5 seconds for the rectangle to complete the animation.

The problem: after the animation starts, switch to another browser tab; after some period of time (longer than 5 seconds) switch back to the first tab.

Expected result: the rectangle continues its path from the point where it left off.

Actual result: most of the times the rectangle appears in its final destination and stops. Sometimes it works as expected. The video demonstrates the problem.

I played with different values for animation-fill-mode and animation-timing-function, but the result was always the same. As rv7 pointed out, sharing the examples in CodePen, JSFiddle, JSBin and stackoverflow JS tool affects the results, so it's better to test directly against a static page on a local HTTP server (or using links below).

For convenience I've deployed the code above to Heroku. The app is a static nodejs HTTP server, which runs on a free subscription, so it may take up to 5 minutes for the page to load for the first time. Testing results against this page:

FAIL Chrome 64.0.3282.167 (Official Build) (64-bit) Linux Debian Stretch (PC)
FAIL Chrome 70.0.3538.67 (Official Build) (64-bit) Windows 10 (PC)
FAIL Chrome 70.0.3538.77 (Official Build) (32-bit) Windows 7 (PC)
FAIL Chrome 70.0.3538.77 (Official Build) (64-bit) OSX 10.13.5 (Mac mini)
FAIL FF Quantum 60.2.2esr (64-bit) Linux Debian Stretch (PC)
OK Safari 11.1.1 (13605.2.8) (Mac mini)

The page visibility API can be replaced with window focus and blur events like this:

window.addEventListener("focus", function() {
    document.querySelector("#test").classList.remove("paused");        
});

window.addEventListener("blur", function() {
    document.querySelector("#test").classList.add("paused");
});

This replacement isn't equivalent however. If a page contains IFRAMEs, interacting with their contents will trigger focus and blur events on the main window. Executing any tab switching code in this case is not correct. This might still be an option in some cases, so I deployed a page for testing here. The results are slightly better:

FAIL Chrome 64.0.3282.167 (Official Build) (64-bit) Linux Debian Stretch (PC)
FAIL Chrome 70.0.3538.67 (Official Build) (64-bit) Windows 10 (PC)
FAIL Chrome 70.0.3538.77 (Official Build) (32-bit) Windows 7 (PC)
FAIL Chrome 70.0.3538.77 (Official Build) (64-bit) OSX 10.13.5 (Mac mini)
OK FF Quantum 60.2.2esr (64-bit) Linux Debian Stretch (PC)
OK Safari 11.1.1 (13605.2.8) (Mac mini)
This version fails more often in Chrome 64-bit than in Chrome 32-bit. In Chrome 32-bit I got just 1 failure after ~20 successful attempts. This might be related to the fact that Chrome 32-bit is installed on older hardware.

The question: is there a way to reliably pause/resume CSS animations when switching tabs?

like image 454
eugenesqr Avatar asked Oct 19 '18 13:10

eugenesqr


2 Answers

You can go with the focus and blur events rather than visibilitychange event because of a better browser support to the former!

let box = document.querySelector('#test');

window.addEventListener('focus', function() {
  box.style.animationPlayState = 'paused';
});

window.addEventListener('blur', function() {
  box.style.animationPlayState = 'running';
});

Alternatively, you can also do this using CSS classes:

.paused {
  animation-play-state: paused !important;
  -webkit-animation-play-state: paused !important;
  -moz-animation-play-state: paused !important;
}
let box = document.querySelector('#test');

window.addEventListener('focus', function() {
  box.classList.remove('paused');
});

window.addEventListener('blur', function() {
  box.classList.add('paused');
});

The above two ways doesn't works in the iframe of CodePen, JSFiddle, JSBin, etc; a possible reason is provided at the end of post. But, here is a video link displaying how the code works in debug mode of CodePen.

Live Example

Confirmed in:

  1. Google Chrome v70.0.3538.67 (Official Build) (32-bit)

  2. Firefox Quantum v62.0.3 (32-bit)

  3. Internet Explorer v11+


Possible reason of why the code doesn't works inside iframe:

When I tried accessing the root window (aka parent object, note that it is not the iframe window), the following error logged in the console:

enter image description here

And this means that I can't access the root window of CodePen and others. Therefore, if I can't access it, I can't add any event listeners to it.

like image 91
vrintle Avatar answered Nov 17 '22 03:11

vrintle


You don't need to use any classes or toggle them to get the expected result. Here, download this gist html file and try at your end, let's see if this works.

You need to set the animation-play-state while writing CSS for the test div to paused.

Once the window loads, set the animation-play-state to running then set them again according to blur and focus event.

Also I noticed using the browser prefix (-webkit-animation-play-state) helped me to get the expected result, so make sure to use them. I tested on firefox and chrome, works fine for me.

Note: This might not work on this code snippet here (since the snippet is not in the main window of this current tab). Download the gist link provided above and check at your end.

window.onload = function() {
  // not mentioning the state at all does not provide the expected result, so you need to set 
  // the state to paused and set it to running on window load
  document.getElementById('test').style.webkitAnimationPlayState = "running";
  document.getElementById('test').style.animationPlayState = 'running';
}

window.onblur = function() {
  document.title = 'paused now';
  document.getElementById('test').style.webkitAnimationPlayState = "paused";
  document.getElementById('test').style.animationPlayState = 'paused';
  document.getElementById('test').style.background = 'pink'; // for testing
}

window.onfocus = function() {
  document.title = 'running now';
  document.getElementById('test').style.webkitAnimationPlayState = "running";
  document.getElementById('test').style.animationPlayState = 'running';
  document.getElementById('test').style.background = 'red'; // for testing
}
div#test {
  width: 50px;
  height: 50px;
  background-color: red;
  position: absolute;
  left: 10vw;
  animation-name: move;
  animation-duration: 5s;
  animation-fill-mode: forwards;
  animation-timing-function: linear;
  /* add the state here and set it to running on window load */
  -webkit-animation-play-state: paused;
  animation-play-state: paused;
}

@keyframes move {
  to {
    left: 90vw
  }
}
<div id="test"></div>
like image 1
Towkir Avatar answered Nov 17 '22 03:11

Towkir