Transform Establishes Containing Block for Descendants

A problem came up when I was trying to draw a heart using CSS following this guide.

You draw a box first. And then you draw two circles attached to two adjacent sides of the box. Finally, you rotate the square by -45 degrees to get the heart upright (but the box is standing on its foot). The whole idea is presented fairly straightforwardly by this diagram:

For learning purposes, I tried to figure out which side is which alright. So I went ahead and changed the colors of my ::before and ::after:

.heart:before {
  top: -15px;
  background-color: pink;
}
.heart:after {
  right: -15px;
  background-color: teal;
}

It looks like that my ::before (pink) is on the left, my ::after (teal) is on the right, and my heart is rotated counter-clockwise because ::before is supposed to have a negative top offset, and ::after is supposed to have a negative right offset.

To verify this idea, I decided to take out the line that rotates the heart:

.heart {
  /* transform: rotate(-45deg); */
}

I would expect my heart to lie down, right, something like this:

But, uh, no. Not exactly. Once I remove that line and hit "refresh". My heart is broken 😱. Or to be more precise, torn apart.

Before I go on explaining what went wrong, I have a couple of confessions to make.

First, I did not follow the guide exactly. If you copy-and-pasted the code, you'd notice that the one of the rounded side does not show up. This is because it is missing the offset definition for the ::after pseudo-element.

But I assume you'd figured that out according to the diagram by yourself, too.

Second, I gave my .heart element a margin offset so that it displays in the center of my screen:

.heart {
  margin: 300px auto 0;
}

Without this line, the result would look a bit different, but equally wrong.

Now let's try to fix it together.

Attempt Fix

It seems to me that my torn apart heart has the ::before and ::after pseudo-elements positioned with respect to the viewport, not their parent, which is the square .heart.

If you are not familiar with this, I encourage you to read the CSS Specification Visual Formatting Model. And I quote here in section 9.8.4 Absolute positioning that absolute positioned boxes are:

... positioned with respect to its containing block. The containing block for a positioned box is established by the nearest positioned ancestor (or, if none exists, the initial containing block).

In human language (with a bit loss of precision), absolute positioned boxes are placed with repect to its nearest ancestor with position: relative. And if such ancestor does not exist, they are positioned with respect to the root element, which is (roughly) the viewport.

So it seems that my ::before and ::after pseudo-elements could not find their position: relative ancestor, and so are placed with respect to the viewport. They both have a negative offset equal to half of their diameters, so I have them neatly cut in half by the edges of my viewport.

Knowing this, I should be able to fix this by adding the position: relative to my .heart element:

.heart {
  /* transform: rotate(-45deg); */
  position: relative;
}

And, expectedly, my heart is fixed, and is lied down, CodePen: position: relative Establishes Containing Block for Descendants:

https://codepen.io/wgao19/pen/moPpEY

The missing position: relative

An immediate question followed up: Have I not initially put position: relative to my .heart? No, right? How come initially the heart was intact?

The only line of code I changed was:

.heart {
  transform: rotate(-45deg);
}

So something else must have happened in between. What if I try transform: rotate(0deg), without position: relative, and see what happens?

.heart {
  transform: rotate(0deg);
  /* position: relative; */
}

I have a lied-down heart exactly like my previous fix! CodePen: Transform Establishes A Containing Block for All Descendants

https://codepen.io/wgao19/pen/LaNegv

transform other than none establishes a containing block

Turns out that:

any value other than none for the transform property also causes the element to establish a containing block for all descendants.

Says section 3 The Transform Rendering Model of CSS Transforms Module Level 1.

What happens when you transform a box other than the transformation itself?

If you transform something, would you expect whatever inside to be transformed together? If you do, you'd share the same intuition with CSS's intention. This includes:

  • Establishes a containing block: (again with some loss of precision) absolute positioned children will offset according to it, width / height will be calculated according to it, etc.
  • Creates a stacking context: you may not make something look "inserted" to the stacking context even if you set a z-index that falls in the range, because anything inside that stacking context shall be atomic with respect to its outside.

And finally, the transformation may extend, but not shrink, the size of the overflow area. Intuitively, what this is saying is that if you transform a box, whatever was visible will remain visible after the transformation. In this illustration below, the orange part will be the extended (visible) overflow area.

Specs Reading

I hope the exploration of this problem makes it easier for you to read the following sections of the CSS Specifications!