Menu

How CSS Perspective Works

September 9th, 2020

As someone who loves creating CSS animations, one of the more powerful tools I use is perspective. While the perspective property is not capable of 3D effects all by itself (since basic shapes can’t have depth), you can use the transform property to move and rotate objects in a 3D space (with the X, Y, and Z axes), then use perspective to control depth.

In this article, I’ll try to explain the concept of perspective, starting with the very basics, as we work up to a fully animated 3D cube.

The basics of perspective

We’ll start with a simple green square and and we’ll move it on all three axes.

While moving the object on the X and Y axes is pretty straightforward, if we’ll move it on the Z axis, it will look like the square stays exactly the same, and that’s because when the object is moving on the Z axis, the animation moves it closer to us and then further from us, but the size (and location) of the square remains the same. That’s where the CSS perspective property comes into play.

While perspective has no influence on the object when it’s moving on the X or Y axes, when the object is moving on the Z-axis, perspective makes the square look bigger when it moves closer to us, and smaller when it moves further away. Yes, just like in “real” life.

The same effect occurs when we’re rotating the object:

Rotating the square on the Z axis looks like the regular rotation we all know and love, but when we rotate the square on the X or Y axes (without using perspective), it only looks like the square is getting smaller (or narrower) rather than rotating. But when we add perspective, we can see that when the square is rotating, the closer side of the square seems bigger, and the further side looks smaller, and the rotation looks as expected.

Note that when the rotation of the object on the X or Y axes is at 90° (or 270°, 450°, 630°, and so on) it will “disappear” from view. Again, this is happening because we can’t add depth to an object, and at this position, the square’s width (or height) will actually be 0.

The perspective value

We need to set the perspective property with a value. This value sets the distance from the object’s plane, or in other words, the perspective’s strength. The bigger the value, the further you are from the object; the smaller the value, the more noticeable the perspective will be.

The perspective origin

The perspective-origin property determines the position from which you are “looking” at an object. If the origin is centered (which is the default) and the object is moved to the right, it will seem like you are looking at it from the left (and vice versa).

Alternatively, you can leave the object centered and move the perspective-origin. When the origin is set to the side, it’s like you are “looking” at the object from that side. The bigger the value, the further aside it will look.

The transformation

While perspective and perspective-origin are both set on an element’s parent container and determine the position of the vanishing point (i.e. the distance from the object’s plane from the position from which you are “looking” at the object), the object’s position and rotation is set using the transform property, which is declared on the object itself.

If you take a look at the code of the previous example, where I moved the square from one side to the other, you’ll see that I used the translateX() function — which makes sense since I wanted it to move along the X-axis. But notice that it’s assigned to the transform property. The function is a type of transformation that is applied directly to the element we want to transform, but that behaves according to the perspective rules assigned to the parent element.

We can “chain” multiple functions to the transform property. But when using multiple transforms, there three very important things to consider:

  1. When rotating an object, its coordinate system is transformed along with the object.
  2. When translating an object, it moves relative to its own coordinate system (rather than its parent’s coordinates).
  3. The order in which these values are written can (and will) change the end result.

In order to get the effect I was looking for in the previous demo, I first needed to translate the square on the X-axis. Only then I could rotate it. If this had been done the other way around (rotate first, then translate), then the result would have been completely different.

To underscore how important the order of values is to the transform property, let’s take a look at a couple of quick examples. First, a simple two-dimensional (2D) transformation of two squares that both have the same transform values, but declared in a different order:

It’s the same deal even if we’re rotating the squares on the Y axis:

It should be noted that while the order of values is important, we could simply change the values themselves to get the desired result instead of changing the order of the values. For example…

transform: translateX(100px) rotateY(90deg);

…will have the same effect as:

transform: rotateY(90deg) translate<strong>Z</strong>(100px);

That’s because in the first line we moved the object on the X axis before rotating it, but in the second line we rotated the object, changed its coordinates, then moved it on the Z axis. Same result, different values.

Let’s look at something more interesting

Sure, squares are a good way to explain the general concept of perspective, but we really start to see how perspective works when we break into three-dimensional (3D) shapes.

Let’s use everything we’ve covered so far to build a 3D cube.

The HTML

We’ll create a .container element that wraps around a .cube element that, in turn, consists of six elements that represent the sides of the cube.

<div class="container">
  <div class="cube">
    <div class="side front"></div>
    <div class="side back"></div>
    <div class="side left"></div>
    <div class="side right"></div>
    <div class="side top"></div>
    <div class="side bottom"></div>
  </div>
</div>

The general CSS

First, we’ll add some perspective to the parent .container element. Then we’ll sure the .cube element has 200px sides and respects 3D transformations. I’m adding a few presentational styles here, but the key properties are highlighted.

/* The parent container, with perspective */
.container {
  width: 400px;
  height: 400px;
  border: 2px solid white;
  border-radius: 4px;
  display: flex;
  justify-content: center;
  align-items: center;
  perspective: 800px;
  perspective-origin: top right;
}

/* The child element, with 3D tranforms preserved */
.cube {
  position: relative;
  width: 200px;
  height: 200px;
  transform-style: preserve-3d;
}

/* The sides of the cube, absolutely positioned */
.side {
  position: absolute;
  width: 100%;
  height: 100%;
  opacity: 0.9;
  border: 2px solid white;
}

/* Background colors for the cube's sides to help visualize the work */
.front { background-color: #d50000; }
.back { background-color: #aa00ff; }

.left { background-color: #304ffe; }
.right { background-color: #0091ea; }

.top { background-color: #00bfa5; }
.bottom { background-color: #64dd17; }

Transforming the sides

The front side is the easiest. We’ll move it forward by 100px:

.front {
  background-color: #d50000;
  transform: translateZ(100px);
}

We can move the back side of the cube backwards by adding translateZ(-100px). Another way to do it is by rotating the side 180deg then move it forward:

.back {
  background-color: #aa00ff;
  transform: translateZ(-100px);


  /* or */
  /* transform: rotateY(180deg) translateZ(100px); */
}

Like the back, there are a couple of ways we can transform the left and right sides:

.left {
  background-color: #304ffe;
  transform: rotateY(90deg) translateZ(100px);


  /* or */
  /* transform: translateX(100px) rotateY(90deg); */
}

.right {
  background-color: #0091ea;
  transform: rotateY(-90deg) translateZ(100px);


  /* or */
  /* transform: translateX(-100px) rotateY(90deg); */
}

The top and bottom are a little different. Instead of rotating them on the Y axis, we need to rotate them on the X axis. Again, it can be done in a  couple of different ways:

.top {
  background-color: #00Bfa5;
  transform: rotateX(90deg) translateZ(100px);


  /* or */
  /* transform: translateY(-100px) rotateX(90deg); */
}
 
.bottom {
  background-color: #64dd17;
  transform: rotateX(-90deg) translateZ(100px);


  /* or */
  /* transform: translateY(100px) rotateX(90deg); */
}

This gives us a 3D cube!

Feel free to play around with the different options for perspective and perspective-origin to see how they affect the cube.

Let’s talk about transform-style

We’re going to add some fancy animation to our cube, but let’s first talk about the transform-style property. I added it earlier in the general CSS, but didn’t really explain what it is or what it does.

The transform-style property has two values:

  • flat (default)
  • preserve-3d

When we set the property to preserve-3d, it does two important things:

  1. It tells the sides of the cube (the child elements) to be positioned in the same 3D space as the cube. If it is not set to preserve-3d, the default value is set to flat , and the sides are flattened in the cube’s plane. preserve-3d “copies” the cube perspective to its children (the sides) and allows us to rotate just the cube, so we don’t need to animate each side separately.
  2. It displays the child elements according to their position in the 3D space, regardless of their place in the DOM.

There are three squares in this example — green, red, and blue. The green square has a translateZ value of 100px, meaning it’s in front of the other squares. The blue square has a translateZ of -100px, meaning is behind the other squares.

But in the DOM, the order of the squares is: green, red, blue. Therefore, when transform-style is set to flat (or not set at all), the blue square will appear on top, and the green square will be in the back, because that is the order of the DOM. But if we set the transform-style to preserve-3d, it will render according to its position in the 3D space. As a result, the green square will be in front, and the blue square will be in the back.

Animation

Now, let’s animate the cube! And to make things more interesting, we’ll add the animation to all three axes. First, we’ll add the animation property to the .cube. It won’t do anything yet since we haven’t defined the animation keyframes, but it’s in place for when we do.

animation: cubeRotate 10s linear infinite;

Now the keyframes. We’re basically going to rotate the cube along each axis so that it appears to be rolling in space.

@keyframes cubeRotate {
  from { transform: rotateY(0deg) rotateX(720deg) rotateZ(0deg); }
  to { transform: rotateY(360deg) rotateX(0deg) rotateZ(360deg); }
}

The perspective property is really what gives the animation that depth, like we’re seeing the cube roll left and right, as well as forward and backward.

But before now, the value of the perspective property had been constant, and so was the perspective-origin. Let’s see how changing these values affects the appearance of the cube.

I’ve added three sliders to this example to help see how different values affect the cube’s perspective:

  • The left slider sets the value of the perspective property. Remember, this value sets the distance from the object’s plane, so the smaller the value is, the more noticeable the perspective effect will be.
  • The other two sliders refer to the perspective-origin property. The right slider sets the origin on the vertical axis, from top to bottom, and the bottom slider sets the origin on the horizontal axis, from right to left.

Note that while the animation is running, these changes may be less noticeable as the cube itself rotates, but you can easily turn off the animation by clicking the “Run animation” button.

Play around with these values and find out how they affect the appearance of the cube. There is no one “right” value, and these values vary from project to project since they’re dependent on the animation, the size of the object, and the effect you want to achieve.

So what’s next?

Now that you’ve mastered the basics of the perspective property in CSS, you can use your imagination and creativity to create 3D objects in your own projects, adding depth and interest to your buttons, menus, inputs, and anything else you want to “bring to life.”

In the meanwhile, you can practice and enhance your skills by trying to create some complex structures and perspective-based animations like this, this, this, or even this.

I hope you enjoyed reading this article and learned something new in the process! Feel free to leave a comment to let me know what you think or drop me a line on Twitter if you have any questions about perspective or any other topic in this article.

The original post How CSS Perspective Works.