How z-index actually works

Probably almost every one of us at least once in his life used the z-index property. In addition, each developer is sure that he knows how it works. In fact, what could be simpler than operations with integers (comparing and assigning them to elements). But is everything as simple as it seems at first glance?

Perhaps the information I describe below is actually trivial. However, I am sure that many will find it useful for themselves. Those who already knew about it will be able to use this text as a cheat sheet in difficult times. So, welcome under cat.

In fact, a person usually begins to try to figure out a new area for himself in three cases: if he encounters unexpected results at work and does not understand what is happening; if the need arises to go beyond and look at the object from a different angle; and finally, just for fun.

My case is clearly not in the third category. At first, I encountered the first scenario several times in my life when working on different projects; however, he did not fully understand the issue due to laziness and the lack of clear and understandable materials with examples. And then, at the beginning of this year, I began to write a web engine, which made me start reading standards and generally look at how different non-trivial things work in popular browsers, and most importantly, whythey work that way.

Let's start with the simple. What is z-index and what is it for?

Obviously, this is the coordinate along the Z axis, given for some element. The Z axis is directed towards the user. A larger number is the closer element.

Why are z-index integers? It's simple. The range is practically unlimited above and below, so we do not need to use fractional values. Since a real monitor does not have a third dimension (we can only imitate it), we need some dimensionless quantity, the only task of which is to provide a comparison of the elements (that is, orderliness of the set). Integers do an excellent job with this task, while they are clearer than real numbers.

It would seem that this knowledge is enough to start using z-index on the pages. However, not all so simple.

``<divstyle="background: #b3ecf9; z-index: 1"></div><divstyle="background: #b3ecb3; margin-top: -86px; margin-left: 38px; z-index: 0"></div>``

Looks like something went wrong. We made the first block more z-index than the second, so why is it displayed below? Yes, it follows the code earlier - but it would seem that this should play a role only with equal values ​​of z-index.

At this point, it's time to open the CSS2.1 standard, or rather, an appendix to it regarding the handling of overlay contexts. Here is the link .

From this small and very concise text, you can immediately take out a lot of important information.

1. z-index control overlaying not individual elements, but overlay contexts (groups of elements)
2. We cannot arbitrarily manage elements in different contexts relative to each other: the hierarchy works here. If we are already in a “low” context, we will not be able to make its element higher than the element of a “higher” context.
3. z-index does not make sense at all for elements in a normal stream (for which the position property is static). We fell into this trap in the example above.
4. In order for an element to set a new overlay context, it must be positioned, and it must be assigned a z-index.
5. If the element is positioned, but the z-index is not specified, then we can conditionally assume that it is equal to zero (for simple cases it works like this, we will consider the nuances later).
6. And still separate contexts of imposing are set by elements with opacity value less than one. This was done so that the alpha blending can be easily transferred to the last rendering stage for processing by the video card.

But that's not all. It turns out that with elements without a z-index is also not so simple as it may seem.

The process of drawing elements of the context subtree can be divided into several stages (the first two of which are the direct output of the background color and the background image of the current element setting the context).

So, consider the entire list.

3. Output of the child contexts with negative z-index
4. Output of the child block elements in the normal stream (only backgrounds)
5. Output of the child float elements
6. Output of the content of elements in the normal stream: inline and inline-block descendants, inline content within the block descendants , including lines of text *
7. Output of child contexts with zero and auto z-index **
8. Displaying child contexts with positive z-index

* in order to traverse the depth-first
** tree for contexts with z-index: auto, consider all child contexts to be descendants of the current context, that is, pulling them up to the current level is

not so easy , true? You can roughly illustrate this scheme with the following picture:

It is also possible to open an example on the codepen and play around with it yourself .

But that's not all. It would seem that the algorithm is already quite complicated: we need to first pull up the child contexts inside pseudo-contexts (remember the value of auto?), Then sort the two z-index lists by building them into a number series, then go through the child elements: first by block in normal flow, then floating, then inline and inline-block ...

But here we are waiting for two surprises. The first, if you're lucky, will not touch us. It is connected with the fact that the background with frames and the contents of block elements are displayed at different stages - but if our samopisny engine for each text node creates an automatically inline element, then everything will be ok, they will naturally be displayed later.

But the second is not so trivial. It is in the mark
It is a new one that has been created.

have float and inline-block / inline (but not block!) elements.

What does this mean in practice? And this means that we must process them in the same way as the elements with z-index: auto. That is, first, bypassing their subtrees and pulling out the child contexts, placing them at the current level. But otherwise we must treat them as elements that set their context. This means that the entire subtree inside them, which has stretched out after traversing to a linear list, must remain atomic. Or, in other words, we cannot shuffle the order of the elements so that the descendants of such an element “surface” above their parent. And if for child contexts, this is intuitively clear (because the algorithm is recursive), then here it is no longer so much.

Therefore, when writing engine code, it’s necessary to go into trickery so that the float, inline and inline-block elements do not yet reveal their descendants (except for child elements with positioning and z-index, which form overlay contexts), and then run the entire function is recursive, but vice versa, taking into account the fact that child contexts should be skipped during the traversal.

A few examples to demonstrate this phenomenon:

``<divstyle="float: left; background: #b3ecf9;"><divstyle="width: 40px; height: 40px; background: #fff700; position: relative; z-index: -1; top: -20px; left: -20px;"></div></div>``

Here the child element has a z-index and is positioned. It “floats up”, but is displayed under the blue square, since elements with negative z-index are displayed at stage 3, and float elements are displayed at stage 5.

``<divstyle="float: left; margin-top: -30px; background: #b3ecf9;"><divstyle="width: 40px; height: 40px; background: #fff700; position: relative; z-index: 0;"></div></div><divstyle="background: #b3ecb3; margin-top: 52px; margin-left: 38px;"><divstyle="width: 40px; height: 40px; background: #ff0000; position: relative; z-index: 0;"></div></div>``

In this example, the second element (green) appears before the first (blue), and therefore below. However, children are pulled up (because they set their own contexts), so in this case they go in the same order in which they go exactly in the source tree (the order of their ancestors after the transposition is not important!). If the first child element is set to z-index equal to 1, then we will get the following image:

``<divstyle="float: left; background: #b3ecf9;"><divstyle="float: left"><divstyle="width: 40px; height: 40px; background: #fff700; position: relative; z-index: 0;"></div></div></div><divstyle=" background: #b3ecb3; margin-top: 32px; margin-left:  40px;"><divstyle="position: relative"><divstyle="width: 40px; height: 40px; background: #ff0000; position: relative; z-index: 0;"></div></div></div>``

Here, the child contexts are pulled out of both floats and regular blocks, the order being preserved as it was in the source tree.

Finally, the last example:

``<divstyle="background: #b3ecf9;"><divstyle="display: inline-block; width: 40px; height: 40px; background: #fc0;"></div></div><divstyle="background: #b3ecb3; margin-top: -100px; margin-left: 22px;"></div>``

As you can see, it is quite possible to “jump out” of a block element - unlike other cases, and since an inline-block element pops up, it will be displayed last in this document.

As you can see, z-index allows you to perform many interesting tricks (which is at least hiding an element under its immediate parent using a negative z-index in a descendant). I hope this article was useful to you.