Scrolling the Z-axis with CSS 3D Transforms

March 22, 2012

We recently decided to make a gimmicky appeal to designers in the hopes that they’d come talk to us. We knew we wanted to do something a little flashy and cutting edge, so CSS 3D Transforms seemed like a good place to start.
Ultimately I decided I wanted a page that would allow users to scroll along the Z-axis instead of the Y-axis we’re all used to. Instead of simulating movement with scaling I wanted to use the translateZ() transform that’s supported by the latest-and-greatest browsers.
In this article we’ll be recreating the z-scroll effect step-by-step. This gist has the final, complete example.

A quick refresh on CSS transforms

From the W3C CSS Transform spec:

The CSS visual formatting model describes a coordinate system within which each element is positioned. Positions and sizes in this coordinate space can be thought of as being expressed in pixels, starting in the upper left corner of the parent with positive values proceeding to the right and down.
This coordinate space can be modified with the ‘transform’ property. Using transform, elements can be translated, rotated and scaled in two dimensional space.

The 3D transform module extends 2D CSS Transforms to include the Z-axis, allowing for 3D transformations of DOM elements.

Getting Perspective

Before you can transform an element in 3D space you have to set a perspective. The element with the perspective property becomes the “viewport” for any child elements you intend to transform. In this article we’ll be using the -webkit- prefix, but you can use the prefix for browser of your choice (update: cers has forked the gist for mozilla). Here’s what our viewport will look like:

 #viewport {
   -webkit-perspective:100px;
   border: 2px solid black;
   height: 400px;
   width: 400px;
   left:50%;
   margin-left:-200px;
 }
 ...

 <div id="viewport"></div>

The perspective value is the depth between you and the plane at Z=0. Let’s look at an excerpt from the spec first which describes this in more detail:

perspective(depth) specifies a perspective projection matrix. This matrix maps a viewing cube onto a pyramid whose base is infinitely far away from the viewer and whose peak represents the viewer’s position.
The viewable area is the region bounded by the four edges of the viewport (the portion of the browser window used for rendering the webpage between the viewer’s position and a point at a distance of infinity from the viewer). The depth, given as the parameter to the function, represents the distance of the Z=0 plane from the viewer.
Lower values give a more flattened pyramid and therefore a more pronounced perspective effect.

A quick sketch can help us understand what they mean. This is a 2D simplification, showing the pyramid from above:

You, the viewer, are at the peak of the pyramid. The volume of the pyramid defines your field of view.
As you can see, a lower perspective value means the rate at which things “fill up” the field of view is faster, making 3D effects more dramatic:

If the concept is still unclear, Ryan Collins has an excellent visual demonstration of the perspective pyramid in CSS 3D, and how depth effects rendering.

The Purpose of perspective-origin

Sometimes you also want to set the perspective-origin property. This sets the x and y position from which you are viewing the elements in the viewport. Again, a visual example helps us illustrate the point. Below we have the view of the perspective pyramid with an origin shifted on the x-axis:

For our purposes we’ll keep the perspective-origin at the center of the viewport.

#viewport {
  -webkit-perspective-origin:50% 50%;
...

Creating Our Layers

Now that we have our perspective set up we can start creating our layers. We’ll create 3 child div tags that we’ll plot along the Z-axis, these will be the boxes that we scroll through:

 <div id="viewport">
   <div class="box" id="a"></div>
   <div class="box" id="b"></div>
   <div class="box" id="c"></div>
 </div>

Our box class will define some basics – our divs will be 100×100, and centered within the viewport. We use position:absolute so that they’ll disregard the standard flow and overlap each other.

 .box {
   position:absolute;
   width:100px;
   height:100px;
   left:50%;
   top:50%;
   margin-left:-50px;
   margin-top:-50px;
 }

Next, our per-box styles translate each div by an offset along the Z-axis to create the layer effect. Each box is separated by -50px with “C” starting at Z=0.

 #a {
   background-color:rgba(255,0,0,.5);
   border:2px solid red;
   -webkit-transform:translateZ(-100px)

 }
 #b {
   background-color:rgba(0,255,0,.5);
   border:2px solid green;
   -webkit-transform:translateZ(-50px)
 }
 #c {
  background-color:rgba(0,0,255,.5);
  border:2px solid blue;
  -webkit-transform:translateZ(0px)
 }

You can play with the translateZ values in your browser’s inspector to see how the divs move along the Z-axis.

The Camera

You can think of our point-of-view (POV) in the browser window as the “camera” in our 3D scene. To create the effect of moving through space we won’t actually be moving our POV like you would a video camera, instead we’ll be moving the elements in our scene to create the effect that our POV is changing while we actually remain stationary.
First, we’ll set our viewport to position:fixed so we can keep it fixed during scrolling. This lets us capture the scrolling event and use it for our own purposes:

#viewport {
   position:fixed; 
...

We must also set the height of our body so we can have a certain amount of scroll room to allow us to move along the Z-axis. Since this is a toy example, let’s assume your browser window is 400px high (the height of the viewport). Now, we’ll need enough pixels past the window’s height to scroll through our three divs.
The initial distance we must cover along the Z-axis is the distance betwen us and Z=0, as you recall this is defined by our perspective value of 100px. Next we’ll need to scroll 50px between box C and box B, and another 50px between B and A. That gives us a final height of 400 + 100 + 50 + 50 = 600px.

body {height:600px;}

Now that our CSS is in order we can actually start coding. Remember, our goal is to allow the user to scroll along this z-axis, so we must appear to move along it as the user scrolls.
The first thing we’ll need to know is how far the user has scrolled. The difference between the last scroll position and the scroll position at the time a scroll event is fired represents the distance we’re going to cover along the Z-axis. We can get the delta like this:

var scrollPosition = document.body.scrollTop;
function scrollDelta() {
  var newScrollPosition = document.body.scrollTop,
      delta = newScrollPosition - scrollPosition;
  scrollPosition = document.body.scrollTop;
  return delta;
}

Next we’ll use the scroll event to know when the scroll wheel is moving and actually move each div based on the delta. Here’s our moveCamera function:

var boxPositions = [-100, -50, 0];
function moveCamera() {
  var boxes = document.getElementsByClassName("box"),
      delta = scrollDelta();
  for (var i=0,l=boxes.length;i<l;i++) {
    boxPositions[i] += delta;
    boxes[i].style["-webkit-transform"] = "translateZ("+boxPositions[i]+"px)";
  }
}
window.addEventListener("scroll", moveCamera, false);

First, we start with an array of the initial Z positions of each box, in DOM order, so that we can know their initial positions. This could also be initialized by parsing the computed style of each div to extract the translation value we’ve set in CSS.
Our first step in the actual moveCamera function is to get the boxes by their class name, as well as the current scroll delta. Next, we iterate through each of the boxes; in this loop we first increment the Z position of the box by the delta, then we set the -webkit-transform style attribute on the element to translate the element by our new Z value. This will effectively increment or decrement the Z-position of each element by the number of pixels we’ve scrolled. Finally, we attach the event handler.
As we scroll the elements will move toward the viewer along the z-axis, making it appear as if we’re moving through the scene! Necessary? Who knows. Awesome? Undoubtedly. And don’t forget — none of this should be limited to the Z-axis. CSS 3D Transforms let you specify any 3D transformation matrix. So what’s next? The DivDoom engine?