Create a zoomable user interface with CSS transforms

  • Knowledge needed: CSS, Intermediate JavaScript
  • Requires: Text editor, browser that supports CSS3 transforms
  • Project time: Three hours
  • Download source files

This article first appeared in issue 216 of .net magazine - the world's best-selling magazine for web designers and developers.

With CSS3 transforms now supported in most major browsers, we have the delightful opportunity to create innovative layouts and interfaces. No longer are we shackled in our one-dimensional prisons, bound to the tyranny of vertically-scrolling sites.

With the site for BeerCamp at SXSW 2011, we at nclud recognised an ideal opportunity to bend some rules and try something new. I got the idea to leverage CSS transforms for the layout. Instead of the typical vertical scrolling site, where you traversed it downwards, this would could be traversed inwards. This is sort of design pattern has been categorised as a zoomable user interface or ZUI.

The BeerCamp at SXSW 2011 was an experiment in using CSS transforms to create a new interface design pattern

The BeerCamp at SXSW 2011 was an experiment in using CSS transforms to create a new interface design pattern. In this tutorial, we’ll build a zoomable user interface into a simple page. You’ll learn how to use CSS transforms to create a zoomable layout. You’ll also learn how to use JavaScript to hijack scrolling to manipulate the zoom.

Our example page lists the services of a web development shop. The services listed range from the broad to the specific. This content is well suited for a zoomable layout, as the subsequent sections are smaller segments of the previous sections. By placing one visual element inside another, you’re visually communicating the relationship between pieces of content.

Basic layout

Prior to the ZUI layout, the basic layout is designed for browsers that don’t support CSS transforms or have JavaScript disabled. The markup looks like this:

  1. <div id="wrap">
  2. <div id="container">
  3. <ul id="nav">
  4. <li><a href="#web-dev">Web development</a></li>
  5. <li><a href="#front-end">Front-end development</a></li>
  6. <li><a href="#css">CSS</a></li>
  7. <li><a href="#css3">CSS3</a></li>
  8. <li><a href="#transforms">Transforms</a></li>
  9. </ul>
  10. <div id="content">
  11. <section id="web-dev">...</section>
  12. <section id="front-end">...</section>
  13. <section id="css">...</section>
  14. <section id="css3">...</section>
  15. <section id="transforms">...</section>
  16. </div> <!-- #content -->
  17. </div> <!-- #container -->
  18. </div> <!-- #wrap -->

The first draft of the site has the content laid out in the typical vertical pattern. This version is necessary as this layout will be used by browsers that do not support CSS transforms or have JavaScript disabled. The rest of the effects will be built with progressive enhancement.

For the zoomable layout, we need to consider how each section fits inside one another. I’ve chosen a ratio of 3:1 for the proportion between the current section and its subsequent section. This means the parent will have enough room for content, and the child container will still be visible.

Each section will be 900px x 540px so it fits within most browser windows. Subsequent sections will appear to be one-third the full size, 300px x 180px. You’ll notice this space in the centre of each section has been reserved for the subsequent sections to fit inside once CSS transforms are put in place. (See Demo 1: Basic layout.)

Now we can start adding CSS transforms. First let’s add Modernizr so we have more control over how browsers will inherit their styles. I’ve opted to use a custom build from the Modernizr 2.0 beta preview that only tests for CSS 2D transforms and CSS transitions. After adding the Modernizr code to our scripts, we can target browsers that support transforms with .csstransforms in our CSS. To scale each section inside one another, they first need to occupy the same space. This can be done with absolute positioning.

  1. /* absolute positioning */
  2. .csstransforms #container { position: relative; }
  3. .csstransforms #content { position: absolute; }
  4. .csstransforms section { position: absolute; }

Each section needs its own scale set. As the proportion we’re using is 3:1, each section will be one-third the size of the previous. This can be calculated as the inverse ratio to the exponent of the level’s zero-based index:

  1. zoomScale = inverse ratio ^ zero-based-level

CSS3 is the principal technology that enables the ZUI for this page. We are leveraging scale transforms. The scale of the first level, #web-dev, is (1/3) ^ 0 or just 1, so we don’t need to set that superfluous style. The scale of the second level, #front-end, is (1/3) ^ 1 or 1/3 or in decimal 0.3333. The scale of the third level, #css, is (1/3) ^ 2 or 1/9 or in decimal 0.1111. We’ll apply this value to the various vendor-prefix transform CSS properties for all four of our subsequent sections. For the sake of brevity, the code example below only lists the unprefixed transform property, but in your actual code, remember to add all the vendor prefixed versions (-webkit-transform, -moz-transform, etc).

  1. /* level index 1: (1/3) ^ 1 = 1/3 = 0.3333 */
  2. .csstransforms #front-end {
  3. -webkit-transform: scale(0.3333);
  4. -moz-transform: scale(0.3333);
  5. -o-transform: scale(0.3333);
  6. transform: scale(0.3333);
  7. }
  8. /* level index 2: (1/3) ^ 2 ) = 1/9 = 0.1111 */
  9. .csstransforms #css {
  10. -webkit-transform: scale(0.1111);
  11. -moz-transform: scale(0.1111);
  12. -o-transform: scale(0.1111);
  13. transform: scale(0.1111);
  14. }
  15. /* level index 3: (1/3) ^ 3 = 1/27 = 0.0370 */
  16. .csstransforms #css3 {
  17. -webkit-transform: scale(0.037);
  18. -moz-transform: scale(0.037);
  19. -o-transform: scale(0.037);
  20. transform: scale(0.037);
  21. }
  22. /* level index 4: (1/3) ^ 4 = 1/81 = 0.0123456 */
  23. .csstransforms #transforms {
  24. -webkit-transform: scale(0.0123456);
  25. -moz-transform: scale(0.0123456);
  26. -o-transform: scale(0.0123456);
  27. transform: scale(0.0123456);
  28. }

Awesome! The sections have been transformed to fit inside one another, like Russian nesting dolls. (See Demo 2 – Scaled sections.)

Each section is positioned inside one another using CSS scale transforms. You can see the second section within the first, and if you look closely, you'll find the others deeper within

Each section is positioned inside one another using CSS scale transforms. You can see the second section within the first and the others deeper within. Now we need to build a mechanism to let the user zoom in.

To zoom in to a section, we only need to apply its reciprocal scale to the sections’ parent #content. All child sections will scale up accordingly. The scale is equal to the ratio to the exponent of level’s zero-base index. The second section #front-end has a scale of 1/3, so it needs to be scaled 3x to bring it to 100% size.

  1. zoomScale = ratio ^ zero-based-level

The scale to view the third level would be 3 ^ 2 = 9. For the fourth level, the scale would be 3 ^ 3 = 27.

  1. /* view #css3, level index 3 = 3 ^ 3 = 27 */
  2. .csstransforms #content {
  3. -webkit-transform: scale(27);
  4. -moz-transform: scale(27);
  5. -o-transform: scale(27);
  6. transform: scale(27);
  7. }

(See Demo 3 – Fixed zoom.) Applying a scale that increases the size of container will zoom in on its content.

Applying a scale that increases the size of container will zoom in on its content


Leveraging window scrolling is a natural convenient interaction to hook zooming into. Along side clicking and pointing, scrolling is a natural interaction that anyone with a mouse or keyboard uses. Currently there isn’t anything to scroll, since the entire page is self-contained in that 900 x 540 area. But we can fake it by adding an empty element that has height, which will serve as our proxy. The markup will be added after #wrap.

  1. </div> <!-- #wrap -->
  2. <div id="scroller"></div>

In the CSS, set an arbitrary height on #scroller. 4000px works as an approximate height of the page before we add the scale transforms.

  1. .csstransforms #scroller { height: 4000px; }

But we don’t want the content to scroll with the rest of the page, so we can use fixed positioning to fix the actual content in its same place.

  1. /* prevent content from scrolling */
  2. #wrap {
  3. position: fixed;
  4. width: 100%;
  5. }

The page scrolls, but the content remains static. Now the fun begins as we jump into scripting.


The basic idea is that we are going to hijack the scroll event and do something with it.

Hijacking the scroll behavior enables users to zoom in to inner content

As all this script will only need to run if the browser supports CSS transforms, we can encapsulate our entire script in a self-executing function, which will only proceed if CSS transforms are supported.

  1. (function(){
  2. // only proceed if CSS transforms are supported
  3. if ( !Modernizr.csstransforms ) {
  4. return;
  5. }
  6. // CSS transforms supported, continue...
  7. })();

I’m using a constructor design pattern, Zoomer, to do all the work. The Zoomer constructor requires a DOM node, specifically the content container, which will be passed in later. It holds properties like scrolled, which will be the vertical scroll position, levels, which is the zero-based number of sections, and the height of the page in docHeight. Most importantly, we pass it in as an event listener to the window’s scroll event.

  1. // the constructor that will do all the work
  2. function Zoomer( content ) {
  3. // keep track of DOM
  4. this.content = content;
  5. // position of vertical scroll
  6. this.scrolled = 0;
  7. // zero-based number of sections
  8. this.levels = 4;
  9. // height of document
  10. this.docHeight = document.documentElement.offsetHeight;
  11. // bind Zoomer to scroll event
  12. window.addEventListener( 'scroll', this, false);
  13. }

The handleEvent method allows the constructor to be used as an event listener, so we can properly use this in window.addEventListener( 'scroll', this, false). If a method matches the event’s type, that method will be called. So we can bind Zoomer.prototype.scroll to the window’s scroll event.

  1. // enables constructor to be used within event listener
  2. // like obj.addEventListener( eventName, this, false )
  3. Zoomer.prototype.handleEvent = function( event ) {
  4. if ( this[event.type] ) {
  5. this[event.type](event);
  6. }
  7. };

Zoomer.prototype.scroll is where the magic will be happening. We first need to calculate the current position of the scroll, relative to the height of the page.

this.scrolled is a normalised decimal value, from 0 to 1, that represents the position of the scrolled page.

  1. // triggered every time window scrolls
  2. Zoomer.prototype.scroll = function( event ) {
  3. // normalize scroll value from 0 to 1
  4. this.scrolled = window.scrollY / ( this.docHeight - window.innerHeight );
  5. };

We can take that scrolled value and use it for our scale value to zoom into the content. We can use the same maths we applied with the CSS above, except we’re now using a percentage.

Its value goes from 0 to 1, so we need to multiply it by the zero-based number of sections.

  1. <code>zoomScale = ratio ^ ( percentage * levels )</code>

This value can be applied as a CSS transform. We need to set all the vendor-specific CSS properties for transform with the transformValue.

  1. // triggered every time window scrolls
  2. Zoomer.prototype.scroll = function( event ) {
  3. // normalize scroll value from 0 to 1
  4. this.scrolled = window.scrollY / ( this.docHeight - window.innerHeight );
  5. var scale = Math.pow( 3, this.scrolled * this.levels ),
  6. transformValue = 'scale('+scale+')';
  7. = transformValue;
  8. = transformValue;
  9. = transformValue;
  10. = transformValue;
  11. };

All that’s left is to initialise a new Zoomer instance, pass in the #content DOM node, and start it up on:

  1. function init() {
  2. var content = document.getElementById('content'),
  3. // init Zoomer constructor
  4. ZUI = new Zoomer( content );
  5. }
  6. window.addEventListener( 'DOMContentLoaded', init, false );

Now the content zooms when you scroll. And everybody goes “Whooaaaaaaaaa." (See Demo 4 – Scroll to zoom.)

If done correctly, when you scroll to the bottom of the page, you’ll zoom in perfectly to the last section.


Behold what you have accomplished! Not only have you produced a pioneer in the realm of web interface development, but you have built it in such a way that it will be fun to use and engaging to interact with. Well done!

For more on the page navigation and CSS transitions, check out David's write-up at