CSS transform-style Property

Chapter 44 20 mins

Learning outcomes:

  1. What is a 3D rendering context
  2. The transform-style property
  3. A simple example
  4. Nested transform-style example
  5. Sharing perspective using transform-style

Introduction

When working with 3D transformations in CSS, one of the most important things is the concept of transform-style, especially when used alongside the value preserve-3d.

It is used to establish a 3D rendering context and, thus, get a single 3D space to be shared by multiple elements, rather than every element being rendered into the plane of its parent.

In this chapter, we shall understand what exactly is transform-style; what is a 3D rendering context; how its values flat and preserve-3d work; what is meant by a 3D rendering context; and a lot more on this road.

Let's begin without further ado.

What is a 3D rendering context?

Before we learn about transform-style, it's worthwhile learning about the related idea of a 3D rendering context.

By default, the children of a given HTML element don't share a common 3D rendering space. They are rendered 'flat' into the plane of their parent, while the parent itself is rendered flat into the plane of its parent, and so on.

But CSS allows us to change this behavior and instead get a set of elements to share a common 3D space. Such a set of elements (that share a single 3D space) is referred to as a 3D rendering context.

A 3D rendering context is a set of elements sharing a common 3D space.

The entire set of elements forms a single 'context' — an 'environment' — for 3D rendering.

The official W3C spec, CSS Transforms Module Level 2, has a good discussion on what is a 3D rendering context.

To establish a 3D rendering context, we use the transform-style property with the value preserve-3d.

The transform-style property

The transform-style property specifies the way the transformations are applied to a given element.

They can either be flat, i.e. rendered into the plane of the element's parent, or shared, i.e. rendered into a single 3D space.

Syntactically, transform-style has two possible values:

transform-style: flat | preserve-3d;

The default value is flat, which renders the transformations into the plane of the parent of the underlying element.

When the transform-style of an element is set to preserve-3d, the element establishes a 3D rendering context. Thereafter, all the children of that element, including the element itself, participate in the rendering context.

Each element then is rendered into its very own plane. (Note that this doesn't mean that each element has its own 3D space.)

If any child itself has the same style, transform-style: preserve-3d, set on it, it doesn't establish a new context but instead extends the one already established by its parent.

This is desirable behavior because otherwise managing multiple 3D rendering contexts within contexts themselves won't be feasible. After all, browser devs have to come up with a mathematically feasible transformation rendering model.

Now, so far, we've only been hearing that transform-style set to preserve-3d establishes (or extends) a 3D rendering context. But we still don't know what it actually looks like and how it affects the rendering of multiple transformed elements.

This we shall see from the next section onwards.

A simple example

It's time to see the visual effect created by the transform-style property and what it really means for elements to 'share' a common 3D space.

Let's say we have a <section> with a <div> nested inside it, as shown below:

<section>
   <div>A div</div>
</section>

And here are the basic styles applied to these:

section {
   display: inline-block;
   border: 10px solid #aaa;
}

div {
   width: 100px;
   height: 100px;
   background-color: yellow;
}
A div

Now, let's say we rotate the <div> by 60 degrees around the y-axis, along with some perspective:

section {
   display: inline-block;
   border: 10px solid #aaa;
}

div {
   width: 100px;
   height: 100px;
   background-color: yellow;
transform: perspective(500px) rotateY(60deg); }
A div

So far, so good. Notice how the <div> has been rotated such that some part of it going into the screen.

With this in place, let's try rotating the <section> by -60 degrees:

section {
   display: inline-block;
   border: 10px solid #aaa;
transform: perspective(500px) rotateY(-60deg); } div { width: 100px; height: 100px; background-color: yellow; transform: perspective(500px) rotateY(60deg); }

Before we see the output, let's spare a few seconds to think what should it actually be.

The <div> is rotate by 60 degrees while the <section>, on the other hand, is rotated by 60 degrees on the opposite side. So, ideally, we expect the <div> to appear perfectly straight, as it initially does (at least based on what we know uptil this point).

Let's now see the output:

A div

What? We don't see what we expected.

Yes, the <section> is indeed rotated but we don't see the <div> straight, as it initially was, according to our naive reasoning.

Well, we get this output because the <div> and <section> are NOT sharing the same 3D space. That is, they don't form a 3D rendering context.

The <div> has its own 3D space while the <section> has its own. We say that the <div> has been flattened into the plane of the <section> element (in line with the spec's jargon).

There's an even more pronounced way to confirm this. That is to scale up the magnitude of the rotation angles to 90 degrees, as done below:

section {
   display: inline-block;
   border: 10px solid #aaa;
   transform: perspective(500px) rotateY(-90deg);
}

div {
   width: 100px;
   height: 100px;
   background-color: yellow;
   transform: perspective(500px) rotateY(90deg);
}

Can you see anything? No?

Well, we should get nothing displayed because the <section> has been rotated by -90 degrees and because the <div> is rendered into the plane of the <section>, without the ability to protrude out of it.

As before, the reason of no display is because both the <div> and the <section> have their own 3D spaces; they don't share one single 3D space.

Let's now change this using the transform-style property, set to the value preserve-3d:

We need the <section> and its child, the <div>, to share the same 3D space. Henceforth, we'll set the transform-style property on the <section> element. (We shall also try setting it on the <div> for the sake of experimentation later on below and see what happens then.)

Here's our new CSS code, with transform-style and without the <section> element rotated:

section {
   display: inline-block;
   border: 10px solid #aaa;
transform-style: preserve-3d; } div { width: 100px; height: 100px; background-color: yellow; transform: perspective(500px) rotateY(60deg); }
A div

As you can see, as of now, there's nothing special about the addition of transform-style. But there sure will, once we rotate the <section>.

Let's do that now, but by -80 degrees, instead of -60 degrees (we've reserved that for another example to demonstrate another concept related to a 3D rendering context):

section {
   display: inline-block;
   border: 10px solid #aaa;
   transform-style: preserve-3d;
transform: perspective(500px) rotateY(-80deg); } div { width: 100px; height: 100px; background-color: yellow; transform: perspective(500px) rotateY(60deg); }
A div

And there's our 3D rendering context!

See how the <div> protrudes out of the <section>. This makes perfect sense because, initially, the <div> was rotated into the screen, so rotating the whole container would obviously keep this effect in tact now that everything is in the same 3D space.

Amazing, isn't this?

As stated earlier, let's now try shifting transform-style from the <section> element to the <div> element, and see the change, if any.

Here's the new CSS:

section {
   display: inline-block;
   border: 10px solid #aaa;
   transform-style: preserve-3d;
   transform: perspective(500px) rotateY(-80deg);
}

div {
   width: 100px;
   height: 100px;
   background-color: yellow;
   transform: perspective(500px) rotateY(60deg);
transform-style: preserve-3d; }

Note that we've removed transform-style from the <section> element.

A div

As can be seen in this output, unlike before, the <div> doesn't protrude out of the <section>. That's because it's NOT sharing its 3D space with the <section>.

With transform-style: preserve-3d set on <div>, the <div> establishes a 3D rendering context, with all of its children and itself having the same 3D space. But the <section> has its own 3D space.

In other words, we've technically taken ourselves back to the same old configuration, where we didn't have any transform-style applied in the first place.

Since we want the <section> and the <div> to be rendered in the same 3D setting, we ought to apply transform-style on the <section> element. Simple.

Nested elements example

Let's try a bit more involved example, with an element nested inside the <div> as well.

Here's the new HTML:

<section>
   <div>
      <span>A span</span>
   </div>
</section>

We have a <section> containing a <div> containing a <span>.

Here are the styles of these elements:

section {
   display: inline-block;
   border: 10px solid #aaa;
}

div {
   display: inline-block;
   border: 10px solid yellow;
}

span {
   display: inline-block;
   height: 100px;
   width: 100px;
   background-color: pink;
}
A span

To start with, we'll rotate the <span> by 60 degrees around the y-axis:

span {
   display: inline-block;
   height: 100px;
   width: 100px;
   background-color: pink;
transform: perspective(500px) rotateY(60deg); }
A span

So far, so good.

Next up, with this in place, we'll rotate the <div> by -80 degrees:

div {
   display: inline-block;
   border: 10px solid yellow;
transform: perspective(500px) rotateY(-80deg); }
A span

Carefully notice how the <span> isn't coming out of the <div>. You know the reason why — the <span> has its own 3D space while the <div> has its own.

With this set, let's now get the <div> to establish a 3D rendering context, using the style transform-style: preserve-3d:

div {
   display: inline-block;
   border: 10px solid yellow;
transform-style: preserve-3d; }

And now let's try rotate the <div> by -80 degrees again:

div {
   display: inline-block;
   border: 10px solid yellow;
   transform-style: preserve-3d;
   transform: perspective(500px) rotateY(-80deg);
}
A span

As expected, when the <div> rotates now, the <span> is seen to be coming out of it by virtue of its own rotation and the fact that it shares its 3D space with the <div> element.

The story doesn't end here; we are still left to experiment with the <section> in this example.

First off, here's what we get when we rotate the <section> by 70 degrees, without the application of transform-style: preserve-3d:

section {
   display: inline-block;
   border: 10px solid #aaa;
   transform: perspective(500px) rotateY(70deg);
}
A span

Closely gazing over it, the <div> and <span> appear as they do in their initial states but because they altogether have a different 3D space than the one used by <section>, they are rendered into the plane of <section> and can't be seen behind it.

Now, let's make the <section> establish a 3D rendering context and see the difference we get with it. Also, we'll remove the transform-style property from <div>:

section {
   display: inline-block;
   border: 10px solid #aaa;
   transform: perspective(500px) rotateY(70deg);
transform-style: preserve-3d; } div { display: inline-block; border: 10px solid yellow; transform-style: preserve-3d; transform: perspective(500px) rotateY(-80deg); }

Here's the output this gives:

A span

Things aren't that clear for the <span>, hence, we change the rotation angle of the <div> element to visualize it better:

div {
   display: inline-block;
   border: 10px solid yellow;
transform: perspective(500px) rotateY(-120deg); }
A span

Yup, it's better now!

Anyways, talking about the explanation, with the <section> rotated and having established a 3D rendering context itself, its <div> child nicely gets transformed, extending out of it (because of its own rotation).

But closely notice what happened to the <span> element. It's now rendered into the plane of the <div> element and that's because <div> doesn't establish, nor extends a 3D rendering context.

To make the <section>, <div> and <span>, each share the same 3D space, we have to additionally apply transform-style: preserve-3d to the <div> element. In this case, the <div> will 'extend' the 3D rendering context already established by its parent, the <section> element.

Let's try this:

div {
   display: inline-block;
   border: 10px solid yellow;
   transform: perspective(500px) rotateY(-120deg);
transform-style: preserve-3d; }
A span

And voila!

All three elements can be now seen crossing each other by virtue of their rotations and the fact that they are all part of the exact same 3D space (established by the <section>).

CSS 3D transforms are spectacularly amazing!

Shared perspective example

If we head over to the W3C spec on 3D transformations, CSS Transforms Module Level 2, we can see a very brief overview of the algorithm used to render an element in a 3D rendering context.

Following that algorithm, there's an important thing to note regarding the application of perspective() on any element in a 3D rendering context.

Let's learn what that important thing is.

By default, when we apply just the perspective() transform function on a given element and then transform a child of it, that child element won't get the perspective as it's not set on the element itself but is rather on its parent.

But when we enter a 3D rendering context, each element participating in that context gets rendered by accumulating the transformations of all its ancestors up until the point where the context was established.

In effect, this simply means that:

If the perspective() function is used on an element that establishes (or extends) a rendering context, then its children will get the perspective() transformation applied.

In fact, its children will get all the transformations of their ancestors applied, not just perspective(), if any.

Technically, this is kind of similar to (but obviously not at all the same as) the perspective property, which also applies perspective to the children of the element it's set on.

As a reminder, remember that the perspective property doesn't apply perspective to the element it's set on.

Let's take an example.

Consider the following code, where we have a <div> inside a <section> with the usual styling seen in this chapter:

<section>
   <div>A div</div>
</section>
section {
   display: inline-block;
   border: 10px solid #aaa;
}

div {
   width: 100px;
   height: 100px;
   background-color: yellow;
}
A div

Below we apply perspective() to the <section> and rotate the <div> by 45 degrees around the y-axis:

section {
   display: inline-block;
   border: 10px solid #aaa;
transform: perspective(500px); } div { width: 100px; height: 100px; background-color: yellow;
transform: rotateY(45deg); }
A div

Because perspective() on <section> doesn't apply to its children, the rotation of <div> appears flat.

Let's now get the <section> to establish a 3D rendering context, using transform-style: preserve-3d, and see the change we get:

section {
   display: inline-block;
   border: 10px solid #aaa;
   transform: perspective(500px);
transform-style: preserve-3d; } div { width: 100px; height: 100px; background-color: yellow; transform: rotateY(45deg); }
A div

Notice how the <div> now gets the perspective applied.

Here's how this happens:

  • To render the <div>, first the transformations applied to it are processed.
  • Then the transformations applied to its parent, <section>, are processed and amalgamated (specifically, pre-multiplied) with the result of its own transformations.

This amalgamation results in the application of perspective to the <div>, thanks to the perspective() transformation given to the <section>.

If we are not inside a 3D rendering context, as we were in the example before this, there is NO amalgamation of the transformations applied to an element with those of its ancestors (uptil the context's establishment point).

Simple, isn't this?

Pre-multiplication of transformations

If we look up in the spec, each element in a 3D rendering context is rendered by accumulating the transformations at each point in the chain of elements, starting from the element itself upto the one establishing the context.

For every element in the context, this accumulation is done by multiplying the transformation matrix of each element in the chain with an accumulator matrix. The way this multiplication happens is important to note.

That is, the transformation matrix of each element is pre-multiplied with the accumulator matrix (instead of being post-multiplied).

Symbolically, we can express this as follows:

transformationMatrix × accumulatorMatrix

The term 'pre-multiplied' means that the transformation matrix comes first in the matrix multiplication operation.

In practice, this means that if we have a <section> inside a <div>, styled as follows:

section {
   transform-style: preserve-3d;
   transform: perspective(500px);
}

div {
   transform: rotateY(45deg);
}

due to the pre-multiplication, the transformation of the <div> would be equivalent to the following (see the comment):

section {
   transform-style: preserve-3d;
   transform: perspective(500px);
}

div {
   transform: rotateY(45deg);
   /* This effectively becomes: */
   /* transform: perspective(500px) rotateY(45deg) */
}

This is a crucial point to note. Because of pre-multiplication, the perspective() function gets applied to child elements.

If it was a normal multiplication, instead of a pre-multiplication, perspective would've been lost and we would've gotten different outcomes, as the transformations of deeper elements would've been put into effect first.

Going with the same example above, if pre-multiplicatoin wasn't done, we would've gotten the following (see the comment):

section {
   transform-style: preserve-3d;
   transform: perspective(500px);
}

div {
   transform: rotateY(45deg);
   /* If pre-multiplication wasn't done: */
   /* transform: rotateY(45deg) perspective(500px) */
}

The perspective() transformation would've been applied after the rotation, effectively producing absolutely no effect.