Search code examples
javascripthtmlcsscss-animations

Pausing css animation on visibilitychange


the goal is to freeze css animation when user navigates to other tab, so when he navigates back, animation is at the same state as when he left. Setting animationPlayState when visibility changes doesn't work, as the animation progress still continues and animation stops only when page is visible again. Is there a way to do this without using unintuitive hacks or animating everything in js?

<!DOCTYPE html>
<html>
<head>
    <style>
        .progress-container {
            width: 100%;
            height: 30px;
            background-color: #f3f3f3;
        }

        .progress-bar {
            height: 100%;
            width: 0;
            background-color: #4CAF50;
            animation: progress 30s linear;
        }

        @keyframes progress {
            from { width: 0; }
            to { width: 100%; }
        }
    </style>
</head>
<body>
<div class="progress-container">
    <div class="progress-bar" id="progressBar"></div>
</div>
<script>
    document.addEventListener('visibilitychange', function() {
        let progressBar = document.getElementById('progressBar');

        if (document.hidden) {
            progressBar.style.animationPlayState = 'paused';
        }
    });

    window.onload = function() {
        let progressBar = document.getElementById('progressBar');
        progressBar.style.animationPlayState = 'running';
    };
</script>
</body>
</html>


Solution

  • One approach is below, with explanatory comments in the code:

    // simple utility functions, to minimise writing/effort;
    // caching the document:
    const D = document,
      // effectively an alias for document.creatElement(), that allos us
      // to create the element and assign properties:
      create = (tag, props) => Object.assign(D.createElement(tag), props),
      // alias for document.querySelector()/element.querySelector() (the
      // 'context' variable can be either document (default), or an element:
      get = (selector, context = D) => context.querySelector(selector),
      // this is just for the demo, and retrieves the <ol> element for logging:
      log = get('.log'),
      // retrieves the .progress-bar element:
      progressBar = get('.progress-bar');
    
    // using EventTarget.addEventListener() to bind the anonymous Arrow function
    // as the event-handler for the 'visibilitychange' event:
    D.addEventListener('visibilitychange', () => {
      // setting the progress-bar's animationPlayState, using a ternary operator,
      // if the current visibility (after the event) is hidden, we return "paused",
      // otherwise we return "runnning":
      progressBar.style.animationPlayState = document.visibilityState === 'hidden' ? 'paused' : 'running';
      
      // this is purely for the demo, and not otherwise required; here we
      // create an <li> element, and pass in the textContent property, to
      // log the current visibilityState, and the current width (retrieved
      // with window.getComputedStyle()); the created <li> is passed to
      // Element.append(), which appends that <li> to the 'log' element:
      log.append(create('li', {
        textContent: `${document.visibilityState}, at width: ${window.getComputedStyle(progressBar,null).width}`
      }));
    });
    .progress-container {
      width: 100%;
      height: 30px;
      background-color: #f3f3f3;
    }
    
    .progress-bar {
      height: 100%;
      width: 0;
      background-color: #4CAF50;
      animation: progress 30s linear;
    }
    
    @keyframes progress {
      from {
        width: 0;
      }
      to {
        width: 100%;
      }
    }
    <div class="progress-container">
      <div class="progress-bar" id="progressBar"></div>
    </div>
    <!-- this is just to illustrate the demo, and not required in production: -->
    <ol class="log">
    </ol>

    JS Fiddle demo.

    References: