How Nesting 3D Transformed Elements Works

By  on  

Ana Tudor is one of those people whose CodePen profiles you check out and go "holy shit." She's that good. She creates incredible visual effects with CSS, on of my favorites being this infinitely unpacked prism. Below she shares her expertise detailing how to create beautiful, nested 3D transforms!

CSS 3D transforms do not work at all in IE9 and older or in Opera 12 and older (Presto). For detailed support info, check caniuse.com.

CSS preserve-3d is currently in development and should shortly land in Internet Explorer!

CSS animations are incredibly popular right now, and I don't just mean animating a simple color or dimension property, I mean 3D transformations as well; CSS flips and rotating cubes being prime examples. We can find simple CSS snippets and examples for transforms, but it's important that developers understand exactly what's happening. Let me take you step by step through the process of nesting animated 3D transform elements!

Let's say we have a door in a frame:

The HTML is simply:

<div class='container'>
  <div class='frame'>
    <div class='door'></div>
  </div>
</div>

In order to open this door, we add a class of door--open:

<div class='container'>
  <div class='frame'>
    <div class='door door--open'></div>
  </div>
</div>

Now we apply a 3D transform on it (with transform-origin anywhere on its left side):

.door--open {
  transform-origin: 0 0 /*whatever y value you wish*/;
  transform: rotateY(-45deg);
}

The only prefix you probably need for 3D transforms is the -webkit- prefix. IE9 does not support them at all and IE10+ (save for the early previews) supports them unprefixed. Opera 12 and older (Presto) never supported them at all, while Opera Next supports them with the -webkit- prefix. Firefox supports them unprefixed since version 16 (since 2012).

This results in:

It doesn't look very realistic. The property that takes care of this is called perspective and it makes things that are closer appear larger and those that are further away appear smaller.

The perspective property should be applied on the parent of the 3D transformed element. Applying it on any ancestor is going to work in WebKit browsers, but not in Firefox or IE.

So we're going to add a frame--realistic class to our door frame:

<div class='container'>
  <div class='frame frame--realistic'>
    <div class='door door--open'></div>
  </div>
</div>

Now we set a perspective on it. The lower the value, the smaller the distance from your eyes is, thus making the things that are closer appear much bigger than those that are further away.

.frame--realistic {
  perspective: 20em;
}

We get the following result:

Now this looks much better! But we can do more! For example, we can also animate the door in 3D. Just replace the door-open class with a door--ani class in the HTML and add the following CSS:

.door--ani {
  transform-origin: 0 0;
  animation: doorani 1.3s ease-in-out infinite alternate;
}
@keyframes doorani {
  from { transform: rotateY(-43deg); }
  to { transform: rotateY(43deg); }
}

Result:

Now let's say we want to also animate the frame a bit in 3D. That should be simple, right? Just add a frame--ani class to the frame, set an animation on it and move the perspective on the container:

The HTML would become:

<div class='container container--realistic'>
  <div class='frame frame--ani'>
    <div class='door door--ani'></div>
  </div>
</div>

And we would need to add the following CSS:

.container--realistic {
  perspective: 20em;
}
.frame--ani {
  animation: frameani 2s ease-in-out infinite alternate;
}
@keyframes frameani {
  from { transform: rotateY(-30deg); }
  to { transform: rotateY(30deg); }
}

But what we get is:

It doesn't look right anymore. It looks as if the door has been flattened into the plane of its parent. Actually, that's exactly what happens because the default value of the transform-style property (which tells the browser whether the 3D transformed children of a 3D transformed element should keep their own 3D transforms or not) has a default value of flat.

This can be fixed by setting transform-style: preserve-3d on the 3D transformed parent (the frame in our case). And then we get this nice result:

However, IE10/11 only support the flat value for the transform-style property. We can sometimes go around this by chaining all the transforms from the parent (and ancestors in general) onto the deepest 3D transformed element.

For example, let's say we have a slightly rotated cube (without the top and bottom faces for simplicity). The HTML is:

<div class='container container--realistic'>
  <div class='cube'>
    <div class='face'></div>
    <div class='face'></div>
    <div class='face'></div>
    <div class='face'></div>
  </div>
</div>

And the relevant CSS:

.cube {
  position: relative;
  width: 5em; height: 5em;
  transform-style: preserve-3d;
  transform: rotateY(30deg) rotateX(10deg);
}
.face {
  position: absolute;
  width: 100%; height: 100%;
}
.face:nth-child(1) {
  transform: /*rotateY(0deg)*/ translateZ(2.5em /* half the side length, 5em in this case */);
}
.face:nth-child(2) {
  transform: rotateY( 90deg)   translateZ(2.5em);
}
.face:nth-child(3) {
  transform: rotateY(180deg)   translateZ(2.5em);
}
.face:nth-child(4) {
  transform: rotateY(270deg)   translateZ(2.5em);
}

With this code (you can check a more detailed explanation for it if you wish), we get the following result:

To make this work in IE, we need to remove the transform from the cube and chain it before the transforms for each face (and if the cube also had a 3D transformed parent, then we would have to remove that transform from it and chain it onto the transforms for each face before that of the cube). We also move the perspective from the container to the cube. Like this:

.cube--ie {
  perspective: 20em;
  transform: none;
}
.face--ie:nth-child(1) {
  transform: rotateY(30deg) rotateX(10deg) 
             translateZ(2.5em);
}
.face--ie:nth-child(2) {
  transform: rotateY(30deg) rotateX(10deg) 
             rotateY( 90deg) translateZ(2.5em);
}
.face--ie:nth-child(3) {
  transform: rotateY(30deg) rotateX(10deg)
             rotateY(180deg) translateZ(2.5em);
}
.face--ie:nth-child(4) {
  transform: rotateY(30deg) rotateX(10deg)
             rotateY(270deg) translateZ(2.5em);
}

And we get the same result, even in IE:

Not very convenient, but it's not that bad. Not much more code, not much uglier... Well, the problem appears when we want to animate the cube. As long as we can rely on transform-style: preserve-3d working, we simply need to add a cube--ani class and this little bit of CSS:

.cube--ani {
  animation: rot 4s linear infinite;
}
@keyframes rot {
  to { transform: rotateY(-330deg) rotateX(370deg); }
}

However, for IE10/11, we cannot have 3D transforms on the cube itself, so we have chained them onto the faces. Which means that's where we need to animate them. Which means... exactly, one set of keyframes for each face!

.cube--ie {
  animation: none;
}
.cube--ani .face--ie:nth-child(1) {
  animation: rot1 4s linear infinite;
}
@keyframes rot1 {
  to {
    transform: rotateY(-330deg) rotateX(370deg) 
               translateZ(2.5em);
  }
}
.cube--ani .face--ie:nth-child(2) {
  animation: rot2 4s linear infinite;
}
@keyframes rot2 {
  to {
    transform: rotateY(-330deg) rotateX(370deg) 
               rotateY(90deg) translateZ(2.5em);
  }
}
.cube--ani .face--ie:nth-child(3) {
  animation: rot3 4s linear infinite;
}
@keyframes rot3 {
  to {
    transform: rotateY(-330deg) rotateX(370deg) 
               rotateY(180deg) translateZ(2.5em);
  }
}
.cube--ani .face--ie:nth-child(4) {
  animation: rot4 4s linear infinite;
}
@keyframes rot4 {
  to {
    transform: rotateY(-330deg) rotateX(370deg) 
               rotateY(270deg) translateZ(2.5em);
  }
}

All that, in order to achieve the same result:

And if so much code is required when there are only four faces, can you imagine what an awful mess would result when trying to do this for about 100 faces?

You may be wondering about the case with the door, where the parent element (the frame) also has a height and a width and is itself visible. Well, the only solution to have both the door and the frame animated in IE10/11 is to change the HTML such that the frame and the door are siblings and animate them individually, while also having the transforms of the frame chained onto the door.

Ana Tudor

About Ana Tudor

Loves maths, especially geometry. Enjoys playing with code. Passionate about experimenting and learning new things. Fascinated by astrophysics and science in general. Huge fan of technological advance and its applications in all fields. Shows an interest in motor sports, drawing, classic cartoons, rock music, cuddling toys and animals with sharp claws and big teeth. Dreams about owning a real tiger.

Recent Features

Incredible Demos

  • By
    Shake Things Up Using jQuery UI&#8217;s Shake Effect

    Yesterday I created a tutorial showing you how you can shake an element using Fx.Shake, a MooTools component written by Aaron Newton. It turns out that jQuery UI also has a shake effect which can draw attention to an element. The XHTML Exactly the same as...

  • By
    Submit Button Enabling

    "Enabling" you ask? Yes. We all know how to disable the submit upon form submission and the reasons for doing so, but what about re-enabling the submit button after an allotted amount of time. After all, what if the user presses the "stop"...

Discussion

  1. hmm making some games, using only css3

  2. What’s the point of animating every single face when it’s much better and reliable to stick everything on a common reference and just rotating it?

    If you use a 3d program to look a solid on different angles you just move the camera around which is nothing than moving the plane around:
    http://codepen.io/luigimannoni/pen/tvqFL

  3. MacK

    Sorry just missed the IE part of it.

  4. This is a good intro to 3d transforms. Definitely powerful if used correctly. Google ‘3d css engine’ for some cool examples pushing it to it’s current limits.

  5. The standard for html markup is to use double quotes by the way.

  6. Nice article Ana, I can’t even begin to fathom how you discovered that chaining your transforms in this way would result in proper rendering cross browser? – great work.

    The team I work with had a similar challenge working on a project 2 years ago and ended up solving them with some javascript magic that is way beyond me but works amazing across the browser landscape.

    In our case we were trying to create a stacked 3D cube style interface that the user could manipulate with their mouse to twist and move a column of cubes to play different HTML games.

    Ana, Thanks for the great article and a new approach to this challenge. I’m curious to learn what steps lead to the discovery this technique, how did you come about finding this approach?

    If anyone is interested in playing with an interactive CSS 3D cube style menu visit – http://www.atari.com/arcade#!/arcade/atari-promo

    • Ana

      Let’s say you apply a transform on a container with a paragraph of text inside. Even though there are no transforms applied on the paragraph itself, it is still going to be transformed as well, not just the container. So transforms of the parent affect the children because the children’s system of coordinates is relative to that of the parent (and the system of coordinates of an element is affected by any transforms applied on that element). So in this case, if I just want to have a transformed paragraph, I get the same result by applying the transform directly on the paragraph. And if the paragraph itself has a transform, I just chain the transform from the parent before.

  7. very nice examples but dos not work in ie -10 may some external library works thank you

  8. fran

    OMG!!! Really great. The power of css is unbelivable. The power of declarative languages. Ana, you rock lady!

  9. Milmoor

    Very nice article. I was looking for this. But why not supply the solution for the door example as well? You descibe the problem using doors and than solve it for cubes. I know the principle is the same, but seeing is believing ;).

    • Riccardo

      It would probably be very difficult to implement this solution for the door, as the door and the frame are rotating in to 2 different timelines and around 2 different origins.

  10. Alex

    Good work trying to show it step by step so that the differences are apparent. This will help a lot of people. Transform-style is very well explained.

Wrap your code in <pre class="{language}"></pre> tags, link to a GitHub gist, JSFiddle fiddle, or CodePen pen to embed!