A useful property of CSS is the
z-index attribute. This allows you to control how elements are layered visually on top of one another within a webpage. This allows you to ensure that all of your page elements display as intended and there are no visual issues with your design.
However, it's not always quite as straightforward as simply specifying only the
z-index to determine the order of elements as they're rendered to the browser screen. Other factors can come into play. Did you know, for example, that the
opacity attribute can affect the layering order of elements? And that unless you explicitly set an element's
z-index has no effect?
How it all stacks up...
There are many things which can affect the stacking order of elements on a page. Even elements without a
z-index property can be arranged to a certain degree.
For example, here, boxes A B and C are placed in order within the HTML. The 'natural' order of the HTML DOM should place A behind B behind C. But because A has
position: relative explicitly set, it is rendered in front of both B and C.
Tricksy, huh? I've found myself caught out by this feature of CSS before. In a lot of cases, explicitly specifying the
position: attribute has sorted out quite a few rendering issues.
So, this shows us that the
position property plays a big part in determining how the browser renders a page's elements.
By itself though, this doesn't provide a particularly flexible way of layering elements. By specifying the
z-index, you can get a greater degree of control over how an element is positioned in the layers. By specifying both
z-index and setting
position, the browser can create what is known as a stacking context. Once a stacking context is created, the browser adjusts the position of the element within the layers of the page and relative to other
positioned elements according to a specific set of rules.
If we had other child elements within any of those three boxes, they would inherit the stacking context of their parent. e.g. if there were child elements in box B, then the children of box B could never appear in front of box A or in front of box C, unless we explicitly modify the properties of A, B or C, thus altering how they are rendered. This shows how each stacking context exists within its own layer of the document. Elements within a specific stacking context cannot appear in front of elements from another, higher context.
Making things clear - Adding opacity
In this example, we are creating three seperate stacking contexts. There are next/prev buttons to advance the sequence of events. During this sequence, CSS properties are adjusted for each of the three boxes. This causes the layering order to change.
This also demostrates an important fact, that it's not just the
z-index property which can affect stacking order. In this example, we also see how the
opacity property influences the rendering order.
Stepping through the events above, you can see how changing the
opacity value affects how the boxes are layered. This is because
opacity is one of the properties that affects how a page's full design is calculated. The specifics of how this happens, and the reasons why this is the case are fairly complex.
Since an element with opacity less than 1 is composited from a single offscreen image, content outside of it cannot be layered in z-order between pieces of content inside of it. For the same reason, implementations must create a new stacking context for any element with opacity less than 1. If an element with opacity less than 1 is not positioned, implementations must paint the layer it creates, within its parent stacking context, at the same stacking order that would be used if it were a positioned element with ‘z-index: 0’ and ‘opacity: 1’. If an element with opacity less than 1 is positioned, the ‘z-index’ property applies as described in [CSS21], except that ‘auto’ is treated as ‘0’ since a new stacking context is always created. See section 9.9 and Appendix E of [CSS21] for more information on stacking contexts. The rules in this paragraph do not apply to SVG elements, since SVG has its own rendering model ([SVG11], Chapter 3).
So this isn't so much a "planned feature", rather a side effect of how the browser has to handle elements with `opacity < 1 in order to display them properly.
The full list of properties that can affect stacking context is fairly extensive. From https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context:
A stacking context is formed, anywhere in the document, by any element which is either
- the root element (HTML),
- positioned (absolutely or relatively) with a z-index value other than "auto",
- a flex item with a z-index value other than "auto",that is the parent element display: flex|inline-flex,
- elements with an opacity value less than 1. (See the specification for opacity),
- elements with a transform value other than "none",
- elements with a mix-blend-mode value other than "normal",
- elements with a filter value other than "none",
- elements with isolation set to "isolate",
- on mobile WebKit and Chrome 22+, position: fixed always creates a new stacking context, even when z-index is "auto"
- specifing any attribute above in will-change even you don't write themselves directly
- elements with -webkit-overflow-scrolling set to "touch"
Essentially, what this says is that there is one large stacking context which begins with the top-level
<html> element, and if the conditions are right, more stacking contexts are formed within the body of the document. All these stacking contexts are processed alongside one another to determine the final appearance of the page.
The stacking order can sometimes catch you out, and leave you in situations where the end result is counterintuitive to what you'd expect. I find this happens especially if the
position property is omitted from an element. If you find yourself wondering why on earth are my elements rendering like **that??**, you should probably bear this in mind.