Pause/resume CSS animations when switching tabs
Asked Answered
M

4

23

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?

Mader answered 19/10, 2018 at 13:1 Comment(1)
This problem still persists up until now, even though the css prop is supported on all major browsers now. Tried with focus/blur and works a bit better but it fails as well. I gave up on trying to pause it and just decided to restart from the beginning on blur by adding animation:none;Kellie
J
14

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.

Jalbert answered 21/10, 2018 at 18:19 Comment(14)
Here is the video that illustrates the problem with the suggested solution.Mader
Why would the use of jQuery change anything here?Septivalent
@Septivalent the use of jQuery doesn't change much things here. You can also use Javascript equivalent. Btw, does the code runs for you?Jalbert
@rv7 It's still a bit unclear to me. Are you saying the code I originally posted works only occasionally, while the code with blur and focus works all the time for you? Or both versions always work?Mader
@Femel the code you posted doesn't works for me, while the code I posted works for me everytime.Jalbert
@Femel I've added one more way using CSS classes and Javascript. Check if that works for you or not. Btw, it worked for me as usual.Jalbert
@rv7 No, the result is the same. I've updated my question with new code and re-tested everything again. Btw this approach isn't the same as using page visibility API when IFRAMEs come into play.Mader
@Femel The visibility-api app doesn't work. But, the focus-blur app worked out as expected!Jalbert
@rv7, I managed to test both apps on my 10 years old laptop on a latest 32-bit Chrome. The results are very similar to yours, except I managed to fail the focus-blur app just once after almost 20 successful attempts. So the problem is very rare, but it's definitely there. Can you try 64-bit Chrome?Mader
@Femel I agree with the fact that the code fails once in 20 (or more) attempts. But logically, I can't say that it's the code, it could be some runtime-errors or some hidden bugs, etc. Also, I've tested your focus-blur app in my 64-bit laptop, and the results were the same as in the 32-bit device.Jalbert
@Femel is your problem still there? Because, I'm interested in starting another bounty on it, if you're ready for new (possibly better) answers.Jalbert
@rv7 the problem is still there. I use CSS transitions as a workaround, so there is no real need for me to solve this. I can do some testing if any decent answer emerges, but that's about it.Mader
This works for pausing for me but it doesn't re-enable the animation when I come back to the tab. BTW, I am using css animations. If I use the css method, it doesn't work at all.Elytron
Should probably be noted that blur/focus don't do the same as visibilitychange. The former will fire even when the tab may still be visible but another window is focused. The latter will fire when the page isn't visible at all, e.g another browser tab in the same window group is open, or the window is minimized etc. And visibiltychange support is considered full nowadays. Also, you'd want to set the vendor specific versions of animation-play-state before the unprefixed one.Septivalent
P
1

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>
Pythagoreanism answered 27/10, 2018 at 11:14 Comment(0)
P
0

For me your example works most of the time.

However I would use percentages and also add a startpoint in the keyframes:

@keyframes move {
  0% { 
    left: 0px;
  }
  100% {
    left: 500px
  }
}

Also The inactive tab detecting doesn't fire always. An inactive window seems to work better: (with jQuery)

$(window).on("blur focus", function(e) {
    var prevType = $(this).data("prevType");

    if (prevType != e.type) {
        var element = document.getElementById("test");
        switch (e.type) {
            case "blur":
                element.style["animation-play-state"] = "paused";
                break;
            case "focus":
                element.style["animation-play-state"] = "running";
                break;
        }
    }

    $(this).data("prevType", e.type);
})
Pout answered 21/10, 2018 at 13:24 Comment(4)
I removed the starting point for simplicity, in my real scenario it is there. Adding/removing it does not change anything. I tried placing console.log inside tab switching handler and can confirm it is always fired.Mader
When in an iframe it doesn't work but when it's the actual document it does: example. jobse.be/animation.htmlPout
@Pout your solution is harder to test, since the animation is looped. Anyways it seems working in FF, but not in Chrome.Mader
Weird, I validated in IE/FF/ChromePout
E
0

Firefox 95 automaticly pauses CSS transition Animations if the tab is in the background but does not pause javascript at the same time so synchronisation is lost for example if im using a 3s transition to close a menu if its not in use and want to show the menu button after these 3 Seconds the effect is that Firefox shows the animation of the menu closing even the button is allready showing up (if the tab gets focus again after its been in the background for a while) which looks ought

My first solution was to use CSS

  transition-delay: 3s;

instead of Javascript

 window.setTimeout(buttoneinblenden, 3000);

to wait these 3 seconds. This way at least the showing up of the button will be paused as well, while the tab is in the background. The menu will not be closed while the tab is in the background even Javascript has fired correctly.

Then i searched a lot and found this post but i did not want to pause instead i want to stop or abbort the transition animation when the tab is blur in the background. So i changed this script https://developer.mozilla.org/en-US/docs/Web/API/Window/focus_event to

 function pause() {
   document.getElementById("menue").style.transition = "none 0s linear";
 }
 function play() {
   document.getElementById("menue").style.transition = "all 3s linear";
 }
 window.addEventListener('blur', pause);
 window.addEventListener('focus', play);

This way no old, paused animations will be shown, if the tab gets focus again.

Because this is not 100% trustworthy (doesnt fire if you use another program in front of Firefox but half see the Firefox Window and then click directly into it so focus and onclick appear at the same time), therefore use

   document.getElementById("menue").style.transition = "all 3s linear";

again, before you do anything else. Hope it might help someone who has the same issue.

Escaut answered 6/1, 2022 at 7:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.