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 bevisible
orhidden
.document.hidden
: A boolean property, whose value may betrue
orfalse
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:
- I am only including the animation-related CSS.
- 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.