← Back home

Lab - 3D Zooming Using CSS Transforms

This lab was based on the 2011 Beercamp website built by the wizards over at nclud. A lot of the code was taken directly from their site, my main goal was to dissect the code and gain insight on how they made the magic happen. Besides a bit of JavaScript trickery, the rest is fairly simple.

View Lab

The Structure

The basic structure of the site looks like this (obviously a simplified version):

Basic Styles

So let's break this down and see how we style these to give us the perspective of a 3D site. Starting with the two outermost elements:

#wrap - We set this to be fixed position so that when we scroll the window, the elements inside stay where they are in the browser.

Since we still want the ability to scroll in order to trigger the zooming, we add the #scroll-proxy element and apply a large height to it. When we get to the JavaScript you'll see that the actual height of the scroll-proxy doesn't matter, you can tweak this to control the speed of zooming.

#container simply centers our page content (horizontally and vertically).

CSS Perspective and Transforms

Note: For the sake of clarity, I've stripped out the prefixed lines of code. If you want this to actually work in most browsers, you'll need to prefix the experimental CSS3 properties with the appropriate vendor prefix.

Moving onto the fun stuff, we have the #environment element which applies three styles that control how our 3D transforms behave.

perspective (and its prefixed variations) activates our 3D space. Dave Desandro (also the man behind the 2011 Beercamp site) has an excellent overview of perspective. To put it simply, we're applying the perspective to #environment so that we activate 3D space on all of its children. The value that we assign to perspective controls the intensity of the 3D effect. The smaller the number, the greater the intensity.

We then use perspective-origin to move the vanishing point of our 3D space. For this design, I tweaked the origin's y-position until the various levels fit inside of the empty spots built into the design.

I was noticing discrepancies between the way WebKit and Firefox were treating perspective-origin. In order to get around this and to make them behave the same, I added transform: perspective(400px);, which seemed to do the trick.

On our #content element we apply a transform-style of preserve-3D. This indicates that the children of this element should be positioned in 3D-space.

Each of our "levels" is a section, in order to position these on top of each other like a stack, we position them as "absolute". To give the appearance that they're in the background and zoomed out, we change the element's position in 3D-space using the translate3d function, decreasing 1000px on the z-axis each level. Our first level section doesn't need this since we want it be zoomed "100%" from the start.

Making it zoom

The above will give us our 3D appearance, however the zooming functionality isn't there yet. In order to zoom to a deeper level we need to apply a 3D transform to the parent of our levels, the #content element. Remember that we set our second level to be -1000px on our 3D space's z-axis. To bring that element to the front and to zoom the other levels accordingly, we need to increase the z-axis of the levels' parent element by 1000px, which would bring our second level to be at 0 on the z-axis (-1000px + 1000px = 0).

Obviously hardcoding this isn't going to do it since we want the site to zoom in and out when we scroll or click the navigation, so we're going to use JavaScript to dynamically change the z-axis value based on our scroll position.

Hopefully you've been prefixing your CSS transforms. We'll use Modernizr to test if transforms are supported, if 3D transforms are supported, and if they are then we'll use Modernizr to get the proper vendor prefix for our style. Here's all of our code:

So what the hell is going on here? Well, at the top we're setting a bunch of global variables that we'll use throughout our code, hopefully most of it is self-explanatory. When our document is ready we set the rest of our variables. The getScrollTransform tests if 3D transforms are supported by the user's browser and if it is, then we'll use the getScroll3DTransform function to return the appropriate style to apply to our container element. If 3D transforms aren't supported by the user's browser, it will fallback to a 2D transform, using the getScroll2DTransform function. Since I was mainly focused on learning more about the 3D aspect of this experiment I'm only going to focus on those parts, so let's break it down.

zoom is called every time the window is scrolled. Instead of taking the pixel value of scrollTop, we want to normalize it into a value between 0 and 1 in order to prevent any possible conflicts with different functions (such as when calculating the 2D or 3D transform). In order to normalize the scroll value, we take the number of pixels we've scrolled (scrollTop) and divide by the difference between our document height (in our case, this is the height of #scroll-proxy) and the size of our browser window.

getScroll3DTransform is passed a value between 0 and 1, based on the windows scroll position. If we've scrolled halfway down the window, scroll would equal 0.5. We use this value to calculate what our z-axis position should be and then return the new translate3D value.

transformScroll takes the scroll value that we passed to it, calculates the proper translate3D using getScroll3DTransform and then assigns it to our transform style property. If you remember, we set the transformProp variable using Modernizr. This way, if we're viewing the site in Safari, we pass WebkitTransform into our style object.

Can I use 3D Transforms yet?

Update (2/3/2014) - Yes.

Kind of. The latest releases of Firefox, Chrome, and Safari support 3D transforms (with the proper prefixes). 3D transforms are coming to Internet Explorer with IE 10. Since a lot of 3D transforms are still in the experimental phase, you'll likely notice discrepancies among how each browser handles them, as I mentioned before about the differences Firefox and WebKit had. A good fallback is to use Modernizr to test for support and provide a fallback to 2D transforms if 3D transforms aren't yet supported, and ultimately a "no transforms" fallback.