Easily manage projects with monday.com

As some people might know, I've always loved 3D geometry. Which has meant getting drawn towards playing with CSS 3D transforms in order to create various geometric shapes. I've built a huge collection of such demos, which you can check out on CodePen.

Because of this, I've often been asked whether it would be possible to create responsive 3D shapes using, for example, `%`

values instead of the `em`

values my demos normally use. The answer is a bit more complex than a yes/no answer, so I thought it would be a good idea to put it in an article. Let's dive in!

### Responsive 3D shapes using `%`

values

This is possible, right? It is, but it complicates things. To illustrate that, let's take an example. Let's say we want to create a cube.

Before we actually start working on that, two things.

- If you need a refresher of how creating a simple, non-responsive rectangular cuboid with the use of CSS 3D transforms works, you can check out this older article all the way to the bar distribution part. It explains (in extreme detail even) a number of concepts related to how CSS transforms work, the process of building the cuboid with clean and compact code and I'm not going to go through all these things again here.
- Let's recap what
`%`

values mean for a few CSS properties we might want to set on an element.

`%`

values for various CSS properties

For `width`

, `padding`

and `margin`

a `%`

value is relative to the `width`

of the parent of the element on which we are setting these properties.

Let's consider the following situation: we have an element, a child of the `body`

. On this element, we set `%`

value `width`

, `padding`

and `margin`

.

```
.boo {
width: 50%;
padding: 10%;
margin: 5%;
}
```

If we resize the viewport such that the `width`

of the `body`

(the parent of our element) is `300px`

, then our element has a `width`

of `150px`

(`50%`

of `300px`

), a `padding`

of `30px`

(`10%`

of `300px`

) and a `margin`

of `15px`

(`5%`

of `300px`

), as it can be seen when inspecting the element.

For this to work as we've intended, the `width`

of the parent shouldn't depend on that of its children. This is a circularity we should avoid. It happens, for example, if the parent has `position: absolute`

. In this case, it appears that the `width`

of the parent is computed such that the content of our element fits, then our element's `width`

, `padding`

and `margin`

get computed as `%`

values of the parent's `width`

.

For `height`

, a `%`

value is relative to the `height`

of the parent of the element on which we are setting the `height`

property. Let's consider our element is the child of the `body`

, which we've made to cover the entire viewport in `height`

.

```
body { height: 100vh; }
.boo { height: 50%; }
```

If we resize the viewport such that the `height`

of the `body`

(the parent of our element) is `300px`

, then the height of our element is `150px`

(`50%`

of `300px`

), as it can be seen when inspecting the element.

For this to work as we've intended, the `height`

of the parent shouldn't depend on its children. This is another circularity we should avoid. But unlike in the case of `width`

, we find ourselves in this situation whenever we don't explicitly set a `height`

on the parent.

For `transform`

, if a `translate()`

/ `translateX()`

/ `translateY()`

/ `translateZ()`

/ `translate3d()`

function uses a `%`

value, then this value is relative to the size of the element along that axis. Note that these are the axes of the element's local system of coordinates, which gets transformed in 3D along with the element itself. Its `x`

axis always points to the right of the element, its `y`

axis always points to the bottom of the element and its `z`

axis always points out from the front of the element.

A value of `translate(50%)`

or `translate(50%, 0)`

or `translateX(50%)`

or `translate3d(50%, 0, 0)`

moves the element along the `x`

axis by half its own `width`

. This means that the vertical midline of the element ends up being where its right edge was initially. In this live test, the grey box represents the element in its intial position, with no transform applied. The orange box represents the element with a `transform`

of `translateX(50%)`

applied.

A value of `translate(0, 50%)`

or `translateY(50%)`

or `translate3d(0, 50%, 0)`

moves the element along the `y`

axis by half its own `height`

. This means that the horizontal midline of the element ends up being where its bottom edge was initially. In this live test, the grey box represents the element in its intial position, with no transform applied. The orange box represents the element with a `transform`

of `translateY(50%)`

applied.

What about a value of `translateZ(50%)`

or `translate3d(0, 0, 50%)`

? Well, no anomaly here, it moves the element along the `z`

axis by half its size along that axis. But what's the size of the element along the `z`

axis? We're not setting it anywhere, that's for sure, but all elements are flat, contained in a plane, and consequently, their size along their `z`

axis is always `0`

. This means that a translation along the `z`

axis that uses a `%`

value **does nothing**, because any `%`

of `0`

is still `0`

. If we check the computed value of `translateZ(50%)`

, we can see that it's `none`

.

#### Consequences of the way `%`

values work

The first one is that **using % values we cannot create elements whose width depends on the viewport height and creating elements whose height depends on the viewport width isn't that straightforward**.

We wanted to create a cube, so let's see how we can create a square whose edge length is `20%`

of the viewport `width`

. First of all, we make sure our square element has both `width`

and `height`

equal to `0`

. We can set these explicitly, or we can absolutely position our square, which is what we choose to do in this case as it comes in handy later when creating the cube. Then we set the `padding`

on this square element to half the `%`

value we want for the cube's edge length `20%/2 = 10%`

- this makes every `padding`

value (`padding-top`

, `padding-right`

, `padding-bottom`

and `padding-left`

) be `10%`

of the viewport `width`

.

```
$edge-len: 20%;
.square {
position: absolute;
padding: .5*$edge-len;
}
```

Adding up a `padding-left`

of `10%`

and a `padding-right`

of `10%`

gives us `20%`

of the viewport `width`

horizontally across the square. Adding up a `padding-top`

of `10%`

and a `padding-bottom`

of `10%`

gives us `20%`

of the viewport `width`

vertically across the square. The result is a square that scales with the viewport `width`

.

This looks great, but we have to keep in mind that we have zeroed our element's `width`

and `height`

, which means we cannot set `box-sizing: border-box`

on it and adding a `border`

that wouldn't add up to the total space occupied by our square on the screen in this case requires either emulation with an inset `box-shadow`

or subtracting the `border-width`

from the `padding`

using `calc()`

.

```
$edge-len: 20%;
$bw: .5em; // border width
.square {
position: absolute;
border: solid $bw currentColor;
padding: calc(#{.5*$edge-len} - #{$bw});
}
```

Also getting cool effects using `background-clip`

, like a transparent space between the `background`

and `border`

becomes more complicated as well. For a simple spaced out `border`

we need to subtract both the `border-width`

and the `box-shadow`

spread from the `padding`

using `calc()`

. Not to mention we also need a `margin`

equal to the `box-shadow`

spread to fix positioning.

```
$edge-len: 20%;
$bw: .5em; // border width
$s: .75em; // shadow spread
.square {
position: absolute;
margin: $s;
border: solid $bw transparent;
padding: calc(#{.5*$edge-len} - #{$bw + $s});
box-shadow: 0 0 0 $s currentColor;
background: #e18728 padding-box;
}
```

For a double spaced out `border`

, we have no choice but to use a pseudo-element.

```
$edge-len: 20%;
$bo: .25em; // outer border width
$so: .5em; // outer gap width
$bi: 1em; // inner border width
$si: .75em; // inner gap width
$inner-len: calc(100% - #{2*($so + $si + $bo + $bi)});
.square {
position: absolute;
padding: .5*$edge-len;
box-shadow: inset 0 0 0 $bo;
&:before {
position: absolute;
border: solid $bi currentColor;
padding: $si;
width: $inner-len; height: $inner-len;
transform: translate(-50%, - 50%);
background: #e18728 content-box;
content: '';
}
}
```

The second consequence is that **translating such an element along its z axis (forward and backward) by an amount that depends on at least one of its viewport-dependant dimensions (width and height) isn't that straightforward either**.

We have already established that we cannot use a `%`

value to translate an element along its `z`

axis by an amount that depends on a viewport dimension. However, with transforms, we have more than one way of bringing an element into a certain position.

For example, `rotate(53deg) translate(5em)`

is equivalent to `translate(3em, 4em) rotate(53deg)`

, as it can be seen in this live test.

So, while we cannot move our square forward by half of its `width`

using a `translateZ()`

with a `%`

value, we can come up with a transform chain that doesn't use a `translateZ()`

function, but still puts our element right where we want it.

Let's consider the square with no border we got above and let's say we want to move it forward by half its edge length. We have more than one way of doing this.

For example, we could start by rotating it around its `y`

axis by `-90°`

. This rotates both our square and its system of coordinates such that it now faces left and its `x`

axis points towards us, out of the screen.

Next, we translate it along its `x`

axis by `50%`

. The `x`

axis now points towards us, so this means our square gets moved forward by half its edge length. The vertical midline of our square is in the position we want it, but we're seeing our square from the right and we want to see it from the front.

This is why our final step is to reverse the initial rotation. So we rotate our square by `90°`

around its `y`

axis - this makes it face us again, while its `x`

axis points to the right again.

These three steps result in the `rotateY(-90deg) translateX(50%) rotateY(90deg)`

transform chain and they are illustrated by the following demo (click to play):

See the Pen translating a square forward by half its edge length without translateZ - method 1 by Ana Tudor (@thebabydino) on CodePen.

Another transform chain that gives the same result is `rotateX(90deg) translateY(50%) rotateX(-90deg)`

. Just like the previous one, it uses the trick of rotating the element (and its local system of coordinates along with it) such that we make another axis (`y`

in this case) point in the direction that the `z`

axis was before this rotation, translating along this other axis (`y`

) and then finally reversing the first rotation. We can see this illustrated in the Pen below:

See the Pen translating a square forward by half its edge length without translateZ - method 2 by Ana Tudor (@thebabydino) on CodePen.

Note that the previous two demos don't work in Edge.

### Creating a responsive cube using `%`

values

We start with the following structure:

```
<div class='cube'>
<div class='cube--face'></div>
<div class='cube--face'></div>
<div class='cube--face'></div>
<div class='cube--face'></div>
<div class='cube--face'></div>
<div class='cube--face'></div>
</div>
```

We could simplify this with a preprocessor like Haml or Slim or whatever.

```
.cube
- 6.times do
.cube__face
```

I do this because I don't like writing the same thing multiple times and, with something like Haml, I'm not even introducing a loop variable that I'm not using in the loop anyway. It's mostly down to personal preference here since we don't have a lot of code anyway. There are just `7`

HTML elements: the `.cube`

element and its `6`

face children^{1}.

We take the `body`

element to be our scene, so we make it cover the entire viewport `height`

and set a `perspective`

on it. Remember that this is an arbitrary decision, taken just so that we simplify things. We could just as well take our scene to be any element whose `width`

depends on the viewport `width`

in some way.

We then absolutely position our elements, making sure that the `.cube`

is dead in the middle of the scene and it has `perspective-style: preserve-3d`

so that its children can be transformed in 3D as well.

```
body {
height: 100vh;
perspective: 20em;
}
div { position: absolute; }
.cube {
top: 50%; left: 50%;
transform-style: preserve-3d;
}
```

Next step would be to size our cube faces like before, using `padding`

, right? Well, if we do that, we discover that the `padding`

is actually evaluated to `0px`

!

That's because they aren't children of the `body`

anymore, they are children of the `.cube`

element, which is an absolutely positioned `0x0`

element.

We can get past this by sizing the cube element using the `padding`

trick and then making its children the same size.

```
$edge-len: 16%;
.cube {
/* previous styles */
margin: -.5*$edge-len;
padding: .5*$edge-len;
&__face {
top: 0; right: 0; bottom: 0; left: 0;
background: orange;
}
}
```

We've also added a negative `margin`

on the `.cube`

element, so that we have its midpoint dead in the middle of the scene instead of its top left corner.

See the Pen responsive cube using % values - step 1 by Ana Tudor (@thebabydino) on CodePen.

This is a start. All the face elements are the size we wanted them to be and they're right where we wanted them. Now let's see how we can transform the `6`

face elements in 3D such that they form a cube.

We start by dividing the `6`

faces into two categories: `4`

lateral faces and `2`

base faces. We consider the lateral faces to be the right, back, left and front ones and the base ones the top and the bottom. This is an arbitrary decision. We could have taken the lateral ones to be the bottom, front, top and back ones and the base ones to be the left and the right. We take the first `4`

face elements in DOM order to be the lateral ones and the others to be the base ones.

The next thing we do is pick the axis we want to do the translations along, the axis which we want to point towards where on the cube (front, bottom, right...) we want to place our face elements. It cannot be the `z`

axis because we need to work with `%`

values here, so we pick the `x`

axis. Again, remember that this is completely arbitrary - in fact, you could take using the `y`

axis instead as something to try after you finish reading this.

We take the first face element. Without any transforms applied, its `x`

axis points right. So we make it be the face on the right of the cube. We translate it by `50%`

in the positive direction along its `x`

axis. This makes its vertical midline coincide with the vertical midline of the cube's right face. We then rotate it by `90°`

around its `y`

axis to place it into the desired position on the cube (facing right).

See the Pen position face element on the right of cube by Ana Tudor (@thebabydino) on CodePen.

The CSS for this first `.cube__face`

element is:

```
.cube__face:first-child {
transform: translateX(50%) rotateY(90deg);
}
```

For consistency purposes, we can write this in the following equivalent form (a rotation of `0°`

around any axis has no effect):

```
.cube__face:nth-child(1) {
transform: rotateY(0deg) translateX(50%) rotateY(90deg);
}
```

We move on to the second face, which we put on the back of the cube. This means we first rotate it by `90°`

around its `y`

axis so that its `x`

points towards that face (which means towards the back). Then we translate it by `50%`

in the positive direction along its `x`

axis. Now its vertical midline coincides with that of the cube's back face. The final step is to rotate it again by `90°`

around its `y`

axis so that it's in the desired position on the cube (facing back).

See the Pen position face element on the back of cube by Ana Tudor (@thebabydino) on CodePen.

We write down the `transform`

for this face:

```
.cube__face:nth-child(2) {
transform: rotateY(90deg) translateX(50%) rotateY(90deg);
}
```

Now it's the turn of the third face to be positioned. This time, on the left of the cube. We start by rotating it by `180°`

around its `y`

axis so that we can make its `x`

axis point left, where we want to put it. Then we translate it by `50%`

in the positive direction along this `x`

axis. This puts its vertical midline on that of the cube's left face. Finally, we rotate it by `90°`

more around its `y`

axis so that it's positioned correctly on the cube (facing right).

See the Pen position face element on the left of cube by Ana Tudor (@thebabydino) on CodePen.

So the transform chain in this case is:

```
.cube__face:nth-child(3) {
transform: rotateY(180deg) translateX(50%) rotateY(90deg);
}
```

Now we got to the final lateral face! This one we position on the front of the cube. Just as with the previous three, the first thing we need to do is rotate it around its `y`

axis in such a way that it makes its `x`

axis point forward. We've seen before that this can be achieved by a `-90°`

rotation around the `y`

axis. But a `-90°`

rotation puts an element in the same position as a `270°`

one, so, for consistency reasons, we're using `270°`

here. After the `270°`

rotation around the `y`

axis, we translate our element by `50%`

in the positive direction along its `x`

axis. And finally, we rotate it by `90°`

more around its `y`

axis so that it faces forward, not left.

See the Pen position face element on the front of cube by Ana Tudor (@thebabydino) on CodePen.

The transform chain for this last lateral face is:

```
.cube__face:nth-child(4) {
transform: rotateY(270deg) translateX(50%) rotateY(90deg);
}
```

Now we can move on to the base faces. To position the first base face on the bottom of the cube, the first step is to rotate it such that its `x`

axis points in that direction (down). That's a `90°`

rotation around its `z`

axis. The following step is to translate it by `50%`

in the positive direction along this `x`

axis that now points down, bringing its midline onto the cube's bottom face. And lastly, we rotate it by `90°`

around its `y`

axis so that it faces down.

See the Pen position face element on the bottom of cube by Ana Tudor (@thebabydino) on CodePen.

This means that the CSS for positioning this face is:

```
.cube__face(5) {
transform: rotateZ(90deg) translateX(50%) rotateY(50%);
}
```

And we got to the last face, which we put on top of the cube. To do so, we start by rotating such that its `x`

axis points up - that's a `-90°`

rotation around its `z`

axis. Following this, we translate it by `50%`

in the positive direction along this `x`

axis that now points up. This way, we've brought the face's midline on the cube's top face. The final step is to rotate it by `90°`

rotation around its `y`

axis so that it faces up.

See the Pen position face element on the top of cube by Ana Tudor (@thebabydino) on CodePen.

```
.cube__face(6) {
transform: rotateZ(-90deg) translateX(50%) rotateY(50%);
}
```

Putting everything together, we can start to notice some patterns:

```
.cube__face(1) { /* 1 = 0 + 1 */
transform: rotateY(0deg) /* 0° = 0*90° */
translateX(50%) rotateY(50%);
}
.cube__face(2) { /* 2 = 1 + 1 */
transform: rotateY(90deg) /* 90° = 1*90° */
translateX(50%) rotateY(50%);
}
.cube__face(3) { /* 3 = 2 + 1 */
transform: rotateY(90deg) /* 180° = 2*90° */
translateX(50%) rotateY(50%);
}
.cube__face(4) { /* 4 = 3 + 1 */
transform: rotateY(270deg) /* 270° = 3*90° */
translateX(50%) rotateY(50%);
}
.cube__face(5) { /* 5 = 4 + 1 */
transform: rotateZ(90deg) /* 90° = 1*90° = ((-1)^4)*90° */
translateX(50%) rotateY(50%);
}
.cube__face(6) { /* 6 = 5 + 1 */
transform: rotateZ(-90deg) /* -90° = -1*90° = ((-1)^5)*90° */
translateX(50%) rotateY(50%);
}
```

The first thing to notice here is that the last two transform functions in the chain are always the same. The next thing is that we can derive a general formula for the lateral faces and the base faces. For the lateral faces, the first transform function is `rotateY($i*90deg)`

, where `$i`

is the face index. For the base faces, the first transform function is `rotate(pow(-1, $i)*90deg)`

. This means that we can compact it all in a loop, like this:

```
@for $i from 0 to 6 {
.cube__face:nth-child(#{$i + 1}) {
transform:
if($i < 4, rotateY($i*90deg),
rotateZ(pow(-1, $i)*90deg))
translateX(50%) rotateY(90deg);
}
}
```

The result of adding the above code to what we had before can be seen in the following Pen:

See the Pen responsive cube using % values - step 2 by Ana Tudor (@thebabydino) on CodePen.

It doesn't look like anything has changed, but, if we make the faces semitransparent, give them a sort of outline and animate the rotation of the `.cube`

element, it becomes obvious that we now have a 3D shape. A responsive one even!

We could also tweak this a bit so that the scene isn't the `body`

anymore:

See the Pen responsive cube using % values - step 4 (scene != body) by Ana Tudor (@thebabydino) on CodePen.

Now this doesn't seem too bad. For the non-responsive case, the transform chain needed to position a face on the cube has a length of `2`

- a rotation and a `translateZ()`

:

```
@for $i from 0 to 6 {
.cube__face:nth-child(#{$i + 1}) {
transform:
if($i < 4, rotateY($i*90deg),
rotateX(pow(-1, $i)*90deg))
translateZ(.5*$edge-len);
}
}
```

With a responsive cube created using `%`

values, it has a length of `3`

- a rotation and then a `translateX(50%)`

and a `rotateY(50%)`

. That's just `6`

more transform functions in total - we can live with that.

The problem is that the more we want to do, the more complex things get.

### A more complex example

Let's say we want to have something like a Rubik's cube. This is an assembly of cubes, with `3`

along each dimension. That's `3*3*3 = 27`

cubes in total. This means we have the following structure:

```
.assembly
- 27.times do
.cube
- 6.times do
.cube__face
```

Each cube gets created almost as above, with a few differences.

The most important one is that now the cubes' parent isn't the scene anymore, it's the `.assembly`

element. Which is absolutely positioned like everything else in the scene, so by default, its size is `0x0`

. So in this case, we need to size the `.assembly`

relative to the scene `width`

, then make the `.cube`

elements and their faces the same size as the assembly.

```
$edge-len: 8%;
div {
position: absolute;
transform-style: preserve-3d;
}
.assembly {
top: 50%; left: 50%;
margin: -.5*$edge-len;
padding: .5*$edge-len;
transform: rotateX(-30deg) rotateY(30deg);
}
[class*='cube'] {
top: 0; right: 0; bottom: 0; left: 0;
}
```

What we have so far is `27`

cubes, all of them positioned in the middle of the scene:

See the Pen responsive cube assembly using % values - step 0 by Ana Tudor (@thebabydino) on CodePen.

To make something like a Rubik's cube, we need to distribute these cubes along the three dimensions of space. Along each dimension, we have `3`

cubes. One gets translated by its edge length in the negative direction of the axis, the second one stays in place and the third one is translated by its edge length in the positive direction of the axis. This means that the values for these translations are `-100%`

, `0`

and `100%`

. These three values can be written as `-1*100%`

, `0*100%`

and `1*100%`

respectively. Since our indices along each axis are `0`

, `1`

and `2`

, this means that the translation along each axis is the index minus `1`

, all multiplied by `100%`

.

So our basic distribution code looks like this:

```
@for $i from 0 to 3 { // along the x axis
$x: ($i - 1)*100%;
@for $j from 0 to 3 { // along the y axis
$y: ($j - 1)*100%;
@for $k from 0 to 3 { // along the z axis
$z: ($k - 1)*100%;
$idx: $i*3*3 + $j*3 + $k + 1;
.cube:nth-child(#{$idx}) {
transform: translate3d($x, $y, $z);
}
}
}
}
```

The problem is that this code does nothing when using `%`

values. If we get rid of the `$z`

and use just a `translate($x, $y)`

, then we can see the cubes distributed along the first two dimensions.

This distribution along the `x`

and `y`

axes looks perfect in Chrome and Edge. Firefox has 3D order issues, but we can easily fix these with `z-index`

in the static case. **Update**: this 3D order bug is now fixed in Firefox 55+.

With the way we've rotated our assembly, the `x`

axis points back and we want the cubes in the back to be behind those in the front. So the higher the `$i`

value, the lower the `z-index`

should be - this means we add it with minus when we compute the value. The `y`

axis points down and we want the cubes on higher up to be above those below. So the higher the `$j`

value, the lower the `z-index`

should be - this means we also add it with minus. The `z`

axis points right and we want the cubes on the left to be behind those on their right. This means that the higher the `$k`

value, the higher the `z-index`

, so we need to add it with plus. We get that:

`z-index: $k - $i - $j;`

Now it also looks fine in Firefox:

See the Pen responsive cube assembly using % values - step 3 by Ana Tudor (@thebabydino) on CodePen.

But what about the third dimension? Well, we can do as before: rotate every cube around its `y`

axis such that its `x`

axis points in the direction its `z`

axis originally pointed and then translate by `$z`

along this `x`

axis. This means our transform chain becomes:

`transform: translate($x, $y) rotateY(90deg) translateX($z);`

This does the trick:

We got our nice, responsive, Rubik's cube structure. But it's at the expense of `2`

extra transform functions per cube. And we have `27`

cubes, so that's `54`

extra transform functions. Our CSS just got a lot bigger.

### Responsive 3D shapes using viewport units

This should be easier, right? Yes, but... depending on what units we choose and on how we might want to animate our 3D shapes, we could run into bugs.

I like using viewport units better than using `%`

because it doesn't come with the same limitations and complications. I can size the shapes depending on the viewport `height`

, not just on the `width`

. Even better, depending on the smaller viewport dimension! And creating the shapes works just the same as when using `px`

or `em`

, no need to add extra transform functions to the chain.

However, we need to be aware of a few browser issues.

#### Edge doesn't yet support `vmax`

This hasn't bothered me a lot because I've rarely wanted `vmax`

-sized shapes and, on the very rare occasions that I have, I was able to get around that one way or another.

#### Current solutions

The first workaround that comes to mind when wanting to create a square whose size depends on the maximum viewport dimension is to set its `width`

and `height`

to a value with `vw`

units and its `min-width`

and `min-height`

to the same value with `vh`

units.

```
.boo {
width: 20vw; height: 20vw;
min-width: 20vh; min-height: 20vh;
}
```

It works like a charm:

We can make this more maintainable by using Sass:

```
$edge-len-landscape: 20vw;
$edge-len-portrait: $edge-len-landscape*1vh/1vw;
.boo {
width: $edge-len-landscape;
height: $edge-len-landscape;
min-width: $edge-len-portrait;
min-height: $edge-len-portrait;
}
```

Now if we want to change how much the edge length depends on the maximum dimension, we only need to change the value of `$edge-len-landscape`

and we don't have to worry about maybe forgetting to change one value somewhere.

Unfortunately, this is not enough when we want to play in 3D. To position the faces in the middle, we'll want a negative margin that depends on the edge length. To create a cube, we need to translate each face by an amount that depends on the edge length. But which one? The portrait or the landscape one?

We have two options that work today: we can either use an orientation (or an aspect ratio) media query or we could use `%`

values inside the `translate()`

function like we did before.

So let's go back to our cube example.

With the **first method**, we combine the above emulation with the regular `transform`

chains we'd apply in the non-responsive case and we add a media query for the `transform`

part:

```
$edge-len-landscape: 20vw;
$edge-len-portrait: $edge-len-landscape*1vh/1vw;
.cube__face {
margin: -.5*$edge-len-landscape;
width: $edge-len-landscape;
height: $edge-len-landscape;
min-width: $edge-len-portrait;
min-height: $edge-len-portrait;
@for $i from 0 to 6 {
&:nth-child(#{$i + 1}) {
transform: if($i < 4, rotateY($i*90deg),
rotateX(pow(-1, $i)*90deg))
translateZ(.5*$edge-len-landscape);
}
}
@media (orientation: portrait) {
margin: -.5*$edge-len-portrait;
@for $i from 0 to 6 {
&:nth-child(#{$i + 1}) {
transform: if($i < 4, rotateY($i*90deg),
rotateX(pow(-1, $i)*90deg))
translateZ(.5*$edge-len-portrait);
}
}
}
}
```

This looks a like a bit too much, so it's probably better if we use a mixin:

```
$edge-len: 20vw;
@mixin faces($l: $edge-len) {
margin: -.5*$l;
width: $l; height: $l;
@for $i from 0 to 6 {
&:nth-child(#{$i + 1}) {
transform:
if($i < 4, rotateY($i*90deg),
rotateX(pow(-1, $i)*90deg))
translateZ(.5*$l);
}
}
}
.cube__face {
@include faces();
@media (orientation: portrait) {
@include faces($edge-len*1vh/1vw);
}
}
```

The result of the above code is a responsive cube depending on the maximum viewport dimension.

With the **second method**, we use `%`

-valued translations in combination with the `vmax`

-emulating sizing. In addition to the `transform`

chains we had when doing everything with percentages, we also need to add a `translate(-50%, -50%)`

at the beginning to compensate for the edge-dependant negative `margin`

so that our square starts as being positioned in the middle of the scene.

If that's not too clear, consider this: we position all elements absolutely, we put the `.cube`

in the middle of the scene (`top: 50%; left: 50%;`

) and then we size its faces (using the `vmax`

emulation method). But this makes the corner of our faces be in the middle of the scene, which is not what we wanted.

See the Pen absolutely position & relative size #0 by Ana Tudor (@thebabydino) on CodePen.

Even if we don't know what negative margin to apply because we don't know whether we're in portrait or in landscape mode, we can still fix this with a `translate(-50%, -50%)`

- this translates the element to the left by half its `width`

(whatever that may be, we don't need to know it) and up by half its `height`

. This is how we get our cube faces to start from the right position.

See the Pen absolutely position & relative size #1 by Ana Tudor (@thebabydino) on CodePen.

Now we have to distribute the faces on the `.cube`

, but if we simply add the `transform`

chains from the all `%`

case, then they overwrite `transform: translate(-50%, -50%)`

and the `.cube`

isn't positioned properly anymore.

See the Pen cube sized depending on max viewport dimension (no vmax!) - positioning fail by Ana Tudor (@thebabydino) on CodePen.

This is why we need to chain `translate(-50%, -50%)`

before the other `transform`

functions for each face:

```
@for $i from 0 to 6 {
.cube__face:nth-child(#{$i + 1}) {
transform: translate(-50%, -50%)
if($i < 4, rotateY($i*90deg),
rotateZ(pow(-1, $i)*90deg))
translateX(50%) rotateY(50deg);
}
}
```

Doing this gives us the result we were after.

#### Future solutions

At this point, CSS variables are listed as "in development" for Edge so, once they're supported, we should be able to start using them for a much cleaner workaround than the two above. The basic idea would be to use a CSS variable for the edge length. We initially set its value to be a `vw`

one, then changing this value to `vh`

inside a media query does the trick.

```
.boo {
--edge-len: 20vw;
width: var(--edge-len);
height: var(--edge-len);
}
@media (orientation: portrait) {
.boo { --edge-len: 20vh; }
}
```

For creating 3D shapes things get a bit trickier because some properties require values that are not equal to the edge length, but computed from it. For example, for our cube, we need to set on the faces a `margin`

that's equal to half the edge length and them we also need to translate the faces by half the edge length. Solution? `calc()`

to the rescue!

```
.cube__face {
--edge-len: 20vw;
margin: calc(-.5*var(--edge-len));
width: var(--edge-len); height: var(--edge-len);
@for $i from 0 to 6 {
&:nth-child(#{$i + 1}) {
transform: if($i < 4, rotateY($i*90deg),
rotateX(pow(-1, $i)*90deg))
translateZ(calc(.5*var(--edge-len)));
}
}
}
@media (orientation: portrait) {
.cube__face { --edge-len: 20vh; }
}
```

See the Pen cube sized depending on max viewport dimension with CSS variables by Ana Tudor (@thebabydino) on CodePen.

This works perfectly in Chrome and Firefox, but will it work in Edge when variables land there too? Well, there's another problem in Edge. `calc()`

works inside translate functions when used for the translate values along the `x`

and `y`

axes, but not for those along the `z`

axis. If we use `calc()`

inside `translateZ()`

or for the translate value along the `z`

axis (the third argument) inside `translate3d()`

, then the computed value for the transform in Edge ends up being `none`

.

The first workaround that comes to mind is to start with another CSS variable, `--inradius`

, from which we compute `--edge-len`

:

```
.cube__face {
--inradius: 10vw;
--edge-len: calc(2*var(--inradius));
margin: calc(-1*var(--inradius));
width: var(--edge-len); height: var(--edge-len);
@for $i from 0 to 6 {
&:nth-child(#{$i + 1}) {
transform: if($i < 4, rotateY($i*90deg),
rotateX(pow(-1, $i)*90deg))
translateZ(var(--inradius));
}
}
}
@media (orientation: portrait) {
.cube__face { --inradius: 10vh; }
}
```

The inradius for both a cube and any of its square faces is equal to half the edge length.

See the Pen cube sized depending on max viewport dimension with CSS variables #2 by Ana Tudor (@thebabydino) on CodePen.

This should probably work in Edge once CSS variables arrive, but we won't know for sure until that actually happens. **Update**: the 3D shape works in Edge 15, though the faces are flickering for a reason unrelated to CSS variables.

### Using `vmin`

values inside translate functions fails in Edge

This sounds really inconvenient because `vmin`

seems to be the go to unit when wanting to create responsive 3D shapes. Fortunately, this has a handy fix: setting the `font-size`

in `vmin`

and then working with `em`

!

See the Pen is earth flat? (pure CSS 3D) by Ana Tudor (@thebabydino) on CodePen.

### Animating translations that use viewport units fails in most browsers after viewport resize

This isn't a problem if we're not animating translations using viewport units.

However, it's the most problematic issue for me because I often want to animate things beyond a simple rotation of the 3D assembly and I haven't been able to find a fix for this problem.

Consider the following example: we have a rectangle we animate to go from the left of the screen to the right (live demo):

```
.boo {
margin-left: -5em;
width: 10em; height: 7.5em;
animation: ani 1s ease-in-out infinite alternate;
}
@keyframes ani {
to { transform: translate(100vw); }
}
```

Before resizing the viewport, everything looks fine in all browsers.

But if we increase or decrease the viewport `width`

, both Chrome and Safari still animate to the same absolute value as before. It's like the `100vw`

value got replaced by its `px`

equivalent. The rectangle isn't animated to the right of viewport anymore, but beyond it if we've decreased the viewport `width`

and to some point in the middle if we've increased it. And the `font-size`

trick used for the previous Edge issue doesn't help in this case.

Edge does something weird too - after resize, the element is translated to the right of the viewport, then just disappears, then flashes back onto the screen.

Firefox is the only browser that gets this one right.

For a simple 3D example, let's say we have a 3D assembly with two identical cubes.

```
<div class='assembly'>
<div class='cube'>
<div class='cube__face'></div>
<!-- 5 more cube faces here -->
</div>
<div class='cube'>
<div class='cube__face'></div>
<!-- 5 more cube faces here -->
</div>
</div>
```

Or, the DRY preprocessor version:

```
.assembly
- 2.times do
.cube
- 6.times do
.cube__face
```

Their edge length is in `vmin`

units (well, in `em`

units, but with `font-size`

set to a `vmin`

value so that we avoid the Edge bug mentioned before). We put the `.assembly`

dead in the middle of the scene and then we offset the cubes to the right and to the left by a quarter of the viewport width (that's `25vw`

). So the basic styles before we actually do anything in 3D would look something like:

```
$edge-len: 16em;
$offset: 25vw;
body {
height: 100vh;
perspective: 32em;
}
div {
position: absolute;
transform-style: preserve-3d;
}
.assembly { top: 50%; left: 50%; }
.cube {
&:nth-child(1) { left: -$offset; }
&:nth-child(2) { left: $offset; }
&__face {
margin: -.5*$edge-len;
width: $edge-len; height: $edge-len;
background: url($image); /* so we can see it */
}
}
```

The result so far can be seen in the following Pen:

See the Pen colliding cubes - step #0 by Ana Tudor (@thebabydino) on CodePen.

We could compact the cube offsets a bit from this point with an index-based sign switching so that we can set them in a loop. `-$offset = -1*$offset`

and `$offset = 1*$offset`

. Also, `-1 = (-1)^1 = (-1)^(0 + 1)`

and `1 = (-1)*(-1) = (-1)^2 = (-1)^(1 + 1)`

. That gives us:

```
.cube {
@for $i from 0 to 2 {
&:nth-child(#{$i + 1}) {
left: pow(-1, $i + 1)*25vw;
}
}
}
```

The faces are positioned exactly like in the non-responsive case.

```
@for $i from 0 to 6 {
.cube__face:nth-child(#{$i + 1}) {
transform:
if($i < 4, rotateY($i*90deg),
rotateX(pow(-1, $i)*90deg))
translateZ(.5*$edge-len);
}
}
```

We now have the two cubes:

See the Pen colliding cubes - step #1 by Ana Tudor (@thebabydino) on CodePen.

The next step is to animate them so that they collide. This means translating the first one towards the right (in the positive direction of the `x`

axis) and the second one towards the left (the negative direction of the `x`

axis). Translating them by the `$offset`

puts them both right in the middle, overlapping, but that's not what we want. We want the right face of the first cube to touch the left face of the second cube right in the middle. This means that they both need to be half an edge length away from the middle. It's like we've translated them by the `$offset`

in one direction and then by half the edge length in the opposite direction. So the `@keyframes`

look like this:

```
.cube {
animation: move 2s ease-in infinite alternate;
@for $i from 0 to 2 {
&:nth-child(#{$i + 1}) {
left: pow(-1, $i + 1)*25vw;
animation-name: move#{$i + 1};
}
}
}
@keyframes move1 {
to {
transform: translateX(calc(1*(#{$offset} - #{.5*$edge-len})));
}
}
@keyframes move2 {
to {
transform: translateX(calc(-1*(#{$offset} - #{.5*$edge-len})));
}
}
```

We can make this code more efficient by generating the `@keyframes`

within the `.cube`

loop:

```
.cube {
animation: move 2s ease-in infinite alternate;
@for $i from 0 to 2 {
&:nth-child(#{$i + 1}) {
left: pow(-1, $i + 1)*25vw;
animation-name: move#{$i + 1};
}
@at-root {
@keyframes move#{$i + 1} {
to {
transform: translateX(calc(#{pow(-1, $i)}*(#{$offset} - #{.5*$edge-len})));
}
}
}
}
}
```

The result looks great in all browsers. That is... until we resize the viewport. WebKit browsers don't update the translation amount in the keyframes to match the new viewport and neither does Edge.

A more complex situation where this breaks things is when morphing from one 3D shape to another via truncation.

See the Pen tetrahedron truncation sequence (interactive, ~responsive) by Ana Tudor (@thebabydino) on CodePen.

Resizing the viewport in this case causes the triangular faces that open up when we start truncating the tetrahedron from its vertices not to be positioned correctly anymore as their place in 3D is also determined by a translation whose value depends on the tetrahedron's edge length (which uses viewport units so that the entire 3D shape scales as the viewport gets resized).

^{1} I've seen a lot of demos and tutorials adding `.front`

, `.back`

, `.left`

, `.right`

, `.top`

, `.bottom`

classes to these face elements and I personally find that pointless, even damaging. If we rotate the whole cube in 3D, which we often want to do, then the `.front`

face isn't in front from our point of view anymore and that can get confusing. Giving them each a different name is kind of distracting fron the fact that they are all similar entities, it doesn't matter which we put where - the first one by DOM order could be placed on the front of the cube, on the right or on the bottom depending on the generic distribution formula we pick. Using a general index-dependent formula that we can position them all in 3D is another very good reason to ditch these classes.

Hello! Maybe you will be interested in my case with 3D image mapping and perspective correction in this JSfiddle link and description in this SO link.

Very cool and thorough article! Thanks for posting it!

Thank you for sharing knowledge, I find it very helpful,,

Looks amazing. I wonder how would you rewrite this in pure Sass, instead of SCSS.

But anyways, this is a bit above my abilities. If this is the future of CSS, it’s time for me to look for another calling. I am one of those that can’t even learn JS. Frankly, my eyes glazed over once I took a look at the code.

Hey there, I’ve cretaed a js lib some time ago tô generate 3D polygons

It can checked out in the following link

https://github.com/ivanbanov/polygonjs