CSS: z-index can be weird.

Before I start this post, there are three things I want to state:

  1. If you think the “z-index” is quite simple, you probably never bothered to care.
  2. When in doubt, one can always read the official specification
  3. There are multiple good elaborations available already (see bottom of this post), but I was missing a comprehensive list of the most important points.
Quick Motivation (skip that if you only want the facts)

Yes. We know: The web has become a place which it never intended to be. Nowadays, it seems to be accommodate everything. You want live control of measurement devices? 3D camera applications? Advanced data wizardry? Or in my case, a kind of sophisticated layout engine? … Web Dev in 2021 gives you the impression that it is all merely a matter of time (or cost).

But then, there are always the caveats. Some semi-suggestive idea turns out to be not that accessible at all. Our user experience considerations made us implement “kind of basic” windows (in the operation system sense), that appear at times and disappear at other times, and give the user maximum information while maintaining minimum clutter.

Very early on, I noticed that I had to implement my own drag’n’drop functionality, because HTML5 isn’t really there yet. But I consider that as something advanced, which also has its idiosyncrasy in every conceivable use case, so that’s ok.

But then again, a somewhat-native-feeling windowing system (even if they are only rectangles with text) makes use of a seemingly simple thing: That stuff gets drawn over other stuff in the right order. And this comes with certain pecularities.

The painting order of HTML elements is divided into stacking contexts. Stacking contexts can be stacked above or below each other, and most of the times they behave as expected, but sometimes, they are not. So, for the roundup…

Stacking Context – Essential Rules

(This assumes CSS knowledge, but don’t hesitate to comment if you have any questions.)

  • If you set any z-index, you set that z-index within the current stacking context.
    • You can never enter an outside stacking context, only create new ones inside
  • One stacking context as a whole is always either above or below other stacking contexts as a whole
  • The root stacking order (from the <html> element) is as you expect:
    • Further down in the HTML source means more upfront
  • Higher z-index means “more upfront”, but
    • z-index doesn’t mean a thing if your neighboring elements do not live inside the same stacking context!
  • Within any parent stacking context, new child stacking contexts are created by
    • Setting CSS “position” to something other than “static”
    • Setting a z-index different from the default value “auto”
      • For clarity: “z-index: 0;” is nearly the same as “z-index: auto;”, but the latter doesn’t open up a child stacking context, while the former does.
    • Setting CSS “display: flex;” or “display: grid”
    • Setting CSS: “isolation: isolate;” (what is that even?)
    • Setting CSS: “will-change” to something non-initial (what is that even?)
    • Setting one of the “graphically advanced” CSS properties like
      • opacity, transform, filter, mix-blend-mode, clip-path, mask, …

There is more, but maybe this can help you bugtracing. And two meta-points:

  • CSS evolves, so with new features, always have stacking context in mind
  • In a framework context (like the React biosphere), you might not know what your imported dependencies do under the hood. Maybe better isolate them.
Reading recommendations:

They have nice illustrations, too.

If everything fails, go back to square one:

https://www.w3.org/TR/CSS2/visuren.html#propdef-z-index

Be aware.

CSS 3D transforms

Simple 3D objects with CSS only

If you are like me when thinking about 3D in the browser you immediately speak of WebGL. But what most developers forget is that we use simple 3D mechanism in our web sites and applications already: the z-index.
While the z-index in only stacking flat containers above each other. Almost all modern browsers can use CSS to create simple 3D models.
Let’s start with a cuboid.

<div class="container">
  <div id="cuboid">
    <div class="front">1</div>
    <div class="back">2</div>
    <div class="right">3</div>
    <div class="left">4</div>
    <div class="top">5</div>
    <div class="bottom">6</div>
  </div>
</div>

We have 6 sides and for easier recognizing each one each has a number on it. We make them bigger and give them a different background to distinguish them further.

.container {
  width: 300px;
  height: 300px;
  position: relative;
  margin: 0 auto 40px;
  padding-top: 100px;
}

#cuboid {
  width: 100%;
  height: 100%;
  position: absolute;
}

#cuboid div {
  display: block;
  position: absolute;
  border: 2px solid black;
  line-height: 196px;
  font-size: 120px;
  font-weight: bold;
  color: white;
  text-align: center;
}

Until now we didn’t use any 3D transformations. For ordering the sides we rotate and translate each side in its place.

    #cuboid .front  { transform: translateZ(100px); }
    #cuboid .back   { transform: rotateX(-180deg) translateZ(0px); }
    #cuboid .right  { transform: rotateY(90deg) translateZ(150px) translateX(-50px); }
    #cuboid .left   { transform: rotateY(-90deg) translateZ(50px) translateX(50px); }
    #cuboid .top    { transform: rotateX(90deg) translateZ(50px) translateY(50px); }
    #cuboid .bottom { transform: rotateX(-90deg) translateZ(200px) translateY(-50px); }

This brought the back side on top but no 3D visible yet. Further we tell the browser to use 3d on its children and move the scene a bit out.

#cuboid {
  transform-style: preserve-3d;
  transform: translateZ( -100px );
}

Still we are trapped in flatland. Ah we are looking straight onto the front. So we rotate the scene.

#cuboid {
  transform-style: preserve-3d;
  transform: translateZ( -100px ) rotateX(20deg) rotateY(20deg);
}

Now we have depth. Something is quite not right. If we remember one thing from our OpenGL days we need another ingredient to make it look 3D: a perspective.

.container {
  perspective: 1200px;
}

Last but not least we add animation to see it spinning.

#cuboid {
  transform-style: preserve-3d;
  transform: translateZ(-100px) rotateX(20deg) rotateY(20deg);
  animation: spinCuboid 5s infinite ease-out;
}
@keyframes spinCuboid {
  0% { transform: translateZ(-100px) rotateX(0deg) rotateY(0deg); }
  100% { transform: translateZ(-100px) rotateX(360deg) rotateY(360deg); }
}