Go back to home page of Unsolicited Advice from Tiffany B. Brown

A look at the Page Visibility API

You're sitting in your office with your speakers un-muted. Maybe you're making plans for dinner, and decide to check out the web site of that hot new restaurant in the North Trendy neighborhood. Of course, you're not supposed to be making dinner plans while on the clock. So when Busybody McNosypants stops by your cube, you quickly tab to your company's intranet application.

Cue loud, auto-playing music. And a case of embarrassment for you.

Multimedia and resource-intensive animations are precisely the kinds of things the Page Visibility API is designed to help manage. The specification is still a working draft, but is at least partially supported in all major browsers.

Browsers that support this API fire a visibilitychange event (prefixed in some browsers) whenever the browsing context — a browser tab or window — of a document gains and/or loses visibility.

What causes a document to gain or lose visibility? Weeeeell, that depends on the browser. Safari doesn't yet support the Page Visibility API. In Opera 12.10 and Chrome (14.0+), a visibilitychange event gets dispatched whenever the user switches tabs. Firefox (10.0+) and Internet Explorer (10+) do the same. However, IE and Firefox also fire a visibilitychange event when the window is minimized.

In my experiments, Firefox 19+ (I haven't tested earlier versions) also fires a visibilitychange event when the document loads or unloads. In both cases, the values of document.visibilityState and document.hidden were hidden and true, respectively. Inline frames also inherit the visibility of their containing document's browsing context.

Of course, the Page Visibility API does have its limits. Browsers do not fire an event if the user opens a new window that covers the first. Similarly, this API can't tell you whether or not the browser window is the active window. If you switch applications, your document code will be executed as though the browser window was at the fore.

So now that I've burst your bubble and thrown a dozen caveats at your head, let's look at the API.

About the API

As far as DOM APIs go, the Page Visibility API is one of the more straightforward ones to use. It consists of two properties and an event.

  • document.visibilityState: Its value may be visible or hidden.
  • document.hidden: A boolean property, whose value may be true or false
  • visibilitychange: An event that can be listened for.

Two more properties, prerender and unloaded, are defined in the editor's draft of the specification. However, not all browsers support these properties.

To use, you'll need to set an event listener on the document object, and define an event handler. You can then query the document.hidden or document.visibilityState properties to determine the state of the document.

var vischangehandler = function(){
    // Check the document.hidden or document.visibilityState property
    if( document.hidden === true ){
        // Stop the animation. Pause the video. Mute the audio.
    } else {
        // Start or resume the animation, video, etc.
    }    
}
document.addEventListener('visibilitychange', vischangehandler, false);

Keep in mind that in the latest versions of Chrome and Internet Explorer, and older versions of Firefox, these properties and the visibilitychange are still prefixed. A table of prefixed properties and events follows.

visibilityState hidden event
Chrome webkitVisibilityState webkitHidden webkitvisibilitychange
Firefox mozVisibilityState mozHidden mozvisibilitychange
Internet Explorer msVisibilityState msHidden msvisibilitychange

Testing for support

Of course, in order to use the Page Visibility API, you need to test whether or not the browser supports it. A simple document.visibilityState === undefined check works, but it doesn't check for prefixed versions. We might also want to abstract away some of the prefix business. One way to do this is shown below.

(function(){

   function define_property(obj, propertyname, func){
      if( Object.defineProperty ){
         Object.defineProperty( obj, propertyname, { get: func });
      } else {
         obj.__defineGetter__(propertyname, func);
      }
   }

   if( document.visibilityState === undefined ){
      define_property( HTMLDocument.prototype, 'visibilityState', function(){
         var o;
         for(o in document){
            if( (/VisibilityState/).test(o) ){
               return document[o];
            }
         }
      });

      ['moz','webkit'].map(function(p){ 
         var visibilitychange = p+'visibilitychange';

         document.addEventListener( visibilitychange, function(e){
            if( e.type !== 'visibilitychange' ){
               var vischange = document.createEvent('Event');
               vischange.initEvent('visibilitychange', e.bubbles, e.cancelable );
               document.dispatchEvent( vischange );
            }
         }, false);         
      });
   }

   if( document.hidden === undefined ){ 
      define_property( HTMLDocument.prototype, 'hidden', function(){
         var o;
         for(o in document){
            if( (/Hidden/).test(o) ){
               return document[o];
            }
         }
      });   
   }
})();

I've also published this as a Gist.

A working example: CSS Animations

Let's look at a working example of the Page Visibility API using CSS Animations. In this example, we will rotate three divs along the y-axis. When our page isn't visible, we will pause the animation (it's a very simple one) and log the status in the console. First our CSS. Note here that:

  1. I am only including the animation-related CSS.
  2. I am not using prefixed properties.

Some browsers still require prefixed properties in order for this animation to work.

@keyframes rotatey {
    from {
        transform: rotateY(1deg);
    }

    to {
        transform: rotateY(360deg);
    }       
}
#stage{
    perspective: 500px;
}
.facet{
    animation-duration: 10s;
    animation-iteration-count: infinite;
}
.rotatey .facet{
    animation-name: rotatey;
}

Now in our event handler, we will just toggle the rotatey class (using classList). Again, here I am using un-prefixed properties and events.

var vchandler = function(e){
    var stage = document.getElementById('stage');

    if( document.hidden === false ){
        stage.classList.add('rotatey');
        console.log('Tab is visible');
    } else {
        stage.classList.remove('rotatey');
        console.log('Tab is not visible');
    }
}

// Here we're going to start the animation upon page load.
window.addEventListener('DOMContentLoaded', vchandler,false);
document.addEventListener('visibilitychange', vchandler,false);

View it in action.

Setting and clearing timeouts using setTimeout() and clearTimeout() or starting and stopping animations with requestAnimationFrame() and clearAnimationFrame() work similarly.