Build a rotating 3D carousel with CSS

  • Knowledge needed: HTML, advanced CSS, basic JavaScript
  • Requires: Modernizr, jQuery, Safari browser (not Chrome, for now)
  • Project time: 1 hour
  • Download source files

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

By now we’ve all played with CSS3’s rounded corners, gradients and drop shadows, but these only touch the surface of what’s now possible. CSS transforms, transitions and animations take our beloved HTML elements and contort, tween and mutate them. What was once dogged scripting and glitchy animation is now smooth, hardware accelerated and easy. And whilst purists aren’t happy about using CSS for behavioural declarations, everything just got a whole lot simpler.

There are two sets of CSS transforms, the widely supported 2D ones (working draft) and the more advanced and exciting 3D versions (working draft), which have a hardware acceleration bonus and which we’ll be using in our carousel.

Currently, only Safari browsers will render these correctly (since April 2009 no less). Chrome support is coming soon and early work has started at Mozilla. Fingers crossed for 2D transforms in IE9, but 3D won’t happen for a long while yet. Importantly, iOS devices (iPhone, iPad and iPod touch) support hardware-accelerated 3D transforms beautifully. Based on browser support, and for brevity, only WebKit-compatible CSS will be used in this tutorial. Please note, however, that the linear gradient on panels doesn't display correctly in the latest webkit because it's using an old syntax.

The basic HTML5 outline

We’re going to create a six-panel rotating carousel, and to keep things simple we’ll make it a fixed width. Let’s start with a basic HTML5 outline. We’ll put all our CSS in carousel.css and JavaScript in carousel.js. We’re also using Modernizr to detect 3D transform capabilities (as well as for the HTML5 shim) and jQuery. When 3D transforms are supported, Modernizr will add a class csstransforms3d to the HTML tag, which we can use in our CSS.

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. ...
  5. <link rel="stylesheet" type="text/css" href="css/carousel.css" />
  6. <script src="js/modernizr-1.6.min.js"></script>
  7. </head>
  8. <body> ...
  9. <script src="js/jquery-1.4.4.min.js"></script>
  10. <script src="js/carousel.js"></script>
  11. </body>
  12. </html>

For our carousel we’ll use a list, with each list item being a panel. To navigate the carousel there are previous and next links that will hook into some JavaScript:

  1. <div id="carousel">
  2. <div class="container">
  3. <ul class="panels">
  4. <li>
  5. <div>
  6. <h2>Panel title</h2>
  7. <p>...</p>
  8. </div>
  9. </li>
  10. ... (6 panels total)
  11. </ul>
  12. </div>
  13. </div>
  14. <nav>
  15. <ul>
  16. <li><a href="#prev">&laquo; Previous</a></li>
  17. <li><a href="#next">Next&raquo;</a></li>
  18. </ul>
  19. </nav>

Before we delve into the 3D realm we should have some basic styles set up, so the carousel looks roughly like we might expect. The carousel has a black background, and a fancy radial gradient (from dark grey to black); it’s centred with a light border. The background will only show through when the panels are animating. We’ll need to add overflow:hidden to this later so the element acts like a window, but while building it’s helpful to see everything:

  1. #carousel {
  2. width: 580px;
  3. height: 300px;
  4. background: #000;
  5. background: -webkit-gradient(linear, right bottom, right top, color-stop(1, rgb(70,70,70)), color-stop(0.57, rgb(0,0,0)));
  6. margin: 0 auto;
  7. border: 1px solid #ccc;
  8. }

The containers are absolutely positioned, with a slightly larger width (600px) so we can space out the panels a little and make them more visually appealing during animations:

  1. #carousel .container {
  2. width: 600px;
  3. }
  4. #carousel .panels,
  5. #carousel .panels > li {
  6. width: 600px;
  7. display: block;
  8. position: absolute;
  9. }
  10. #carousel .container,
  11. #carousel .panels > li {
  12. height: 300px;
  13. }

And finally come our panels: 580px total width, the same size as the carousel, but with a little padding, gradient and text-shadow to prettify it.

Our styled carousel and panel before we apply any 3D transforms

During development, making this panel slightly transparent can help solve bugs and illustrate 3D structures more clearly:

  1. #carousel .panels > li > div {
  2. background: #fff;
  3. background: -webkit-gradient(linear, right bottom, right top, color-stop(1, rgb(255,255,255)), color-stop(0.57, rgb(230,230,230)));
  4. height: 260px;
  5. width: 540px;
  6. padding: 20px;
  7. text-shadow: 0 1px 0 #fff;
  8. }

3D needs perspective

To begin our 3D adventure we need to give our carousel some perspective. It’s a one-liner with a hefty explanation:

  1. .csstransforms3d #carousel {
  2. -webkit-perspective: 800;
  3. }

Here's the science part. In three dimensions we have three planes, x, y, and z. x and y run left to right and top to bottom respectively. The z-plane runs from front to back, perpendicular to x and y. The perspective value defines the distance of the z=o plane from the viewer, which affects the magnitude of the 3D effect.

With a low number the z=0 plane is very close, and small changes have a big effect. Imagine standing beneath a skyscraper and looking straight up: you’re small and the object is huge. With a high number, the z=0 plane is far away and changes are more subtle – it’s the same skyscraper, but this time you’re flying way above and looking down. A perspective around the 800 to 1,000 mark feels natural. By default, the perspective’s origin, ie its vanishing point, is centred – this suits us fine, but it can be altered if necessary:

  1. -webkit-perspective-origin: 50% 50%;

The perspective applies to all the child elements, and the element with perspective becomes a containing block. This may sound complicated, but remember: all the difficult perspective calculations are performed by the browser, and after playing with the code it quickly becomes second nature.

One more thing: direct children of an element with perspective will be positioned in 3D space (eg div.container), but their children will be flatted into 2D. We can avoid this by setting the transform-style to preserve-3d:

  1. .csstransforms3d #carousel .container,
  2. .csstransforms3d #carousel .panels {
  3. -webkit-transform-style: preserve-3d;
  4. }

A circle of panels

When we move from panel to panel, the carousel will rotate. For this effect, we first need to position each panel in a circle, using a 3D transformation about the top-to-bottom y-axis. There are six panels in this example, so each will be at 60 degrees to the next:

  1. .csstransforms3d #carousel li:nth-child(1) { -webkit-transform: rotateY(0deg) }
  2. .csstransforms3d #carousel li:nth-child(2) { -webkit-transform: rotateY(-60deg)}
  3. .csstransforms3d #carousel li:nth-child(3) { -webkit-transform: rotateY (-120deg)}
  4. .csstransforms3d #carousel li:nth-child(4) { -webkit-transform: rotateY (-180deg)}
  5. .csstransforms3d #carousel li:nth-child(5) { -webkit-transform: rotateY (-240deg)}
  6. .csstransforms3d #carousel li:nth-child(6) { -webkit-transform: rotateY (-300deg)}

But rotating around just the y-axis will put all panels on top of each:

Oh dear, that's not quite right. The panels are rotated but they're sitting on top of each other

We need to spread them out so edge of each list element touches the next and a circle is formed.

This is simple enough: translating in the z-plane will move each panel away from the centre point. Calculating the exact distance to translate, ie the circle’s radius, requires a little trigonometry. In this case, for six panels of width 600px, it’s about 519px.

Our six panels are calculated in a circle. We can calculate the radius of the circle using trigonometry

Translating 519px will put the panel faces on the outside of the circle: -519px will send them in the opposite direction and they’ll be on the inside. It depends how you want your carousel to behave. In this example we’ll be on the inside looking out, so we’ll use -519px and our final transforms look like:

  1. .csstransforms3d #carousel li:nth-child(1) { -webkit-transform: rotateY(0deg) translateZ(-519px); }
  2. ...

A circle of panels translated by -519px. We're inside the circle and the panels move around us

Alas, there’s another problem: all the panels are in a circle but the front-most has scaled up and looks huge.

We’ve moved the panels in the z-plane. Using our container we can alter where we view the circle from by moving the panels back, ie by translating Z an equal amount but in the opposite direction:

  1. .csstransforms3d #carousel .container {
  2. -webkit-transform: translateZ(519px);
  3. }

After translating our panels by -519px, we're looking at the circle from the wrong position. We need to translate the container

Rotating the circle

Our circle is complete and we’re sitting in the middle of it, but how do we move from one panel to the next?

Rather than change the position of each element we can shift the container. The <ul> looks straight ahead. Rotating it by 60 degrees will move all six panels and bring the next panel into view: -60 degrees will go in the opposite direction and show the previous one.

This can be scripted by accessing the <ul> element’s style property. Try typing something like this into a JavaScript console:

  1. $('#carousel .panels')[0].style.webkitTransform = &ldquo;rotateY(60deg)"

That changes the panel, but there’s no animation. We need to add a transition. Using -webkit-transition (specification: transitions) we’ll tell the browser that when we change from one transform to another we’d like a transition between two states over 500ms:

  1. .csstransforms3d #carousel .panels {
  2. -webkit-transition: -webkit-transform 500ms ease-in-out;
  3. }

Try the JavaScript again and you’ll see the circle smoothly move around from one panel to the next. (Don’t forget to put overflow:hidden back onto #carousel.)


In our JavaScript we can define this behaviour and hook it to clicks on the ‘next’ and ‘previous’ links.

When the page has loaded, and when a link is clicked, depending on the hash of the link we add or subtract 60 from our variable y and update an inline webkitTransform style. The browser does the hard bit:

  1. $(function(){
  2. var y = 0;
  3. $('nav a').click(function(evt) {
  4. evt.preventDefault();
  5. switch( {
  6. case &ldquo;#prev":
  7. y = y - 60;
  8. break;
  9. case &ldquo;#next":
  10. y = y + 60;
  11. break;
  12. default:
  13. break;
  14. };
  15. $('.panels')[0].style.webkitTransform = 'rotateY(' + y + 'deg)';
  16. });
  17. });

Clicking the next and previous links now animates the carousel.

Steps in our animation as the carousel transitions from right to left to show the next panel


So that’s it, a simple 3D carousel to wow your friends with. It even works on an iPhone! There are obviously some caveats: the angles of rotation and transform values are hard-coded to work with six panels of a fixed size, but that’s nothing a little scripting can’t rectify. We should also provide fallbacks for other browsers – the no-csstransforms3d Modernizr class is a useful hook for that.

Hardware accelerated 3D CSS even works smoothly on the iPhone

If you’ve enjoyed this and perhaps want to play a bit more I’ve included an advanced version in the tutorial files. This adds a zoom effect and chains multiple transitions using the webkitTransitionEnd event.

Transitions and transforms

Transforms rotate, scale, translate and skew elements. Transitions define a behaviour for moving from one state to another ( And animations use keyframes to change multiple properties over a time period (


transition: <property> <duration> <timing-function> <delay>

Example: When the opacity changes (eg, on hover from 0.5 to 1, or back again), transition from one value to the next linearly, over 50ms:

-webkit-transition: opacity 50ms linear;
-moz-transition: opacity 50ms linear;


transform: <transform function> [<transform function>]*

A number of transform functions are available to you, the most useful being:

scaleX(), scaleY(), scaleZ(), shorthand: scale(x,y), scale3d(x,y,z)

rotateX(), rotateY(), rotateZ(), shorthand: rotate(x,y), rotate3d(x,y,z)

skewX(), skewY(), shorthand: skew(x,y)

translateX(), translateY(), translateZ(), shorthand: translate(x,y), translate3d(x,y,z)

-webkit-transform: rotateX(-10deg) rotateY(20deg); -webkit-transform: translateX(10px) rotate3d(20deg, 20deg, 5deg);