Putting things on top of other things
This is a post about stacking contexts: what they are, when they happen, and why.
Whys in CSS are not often discussed, as they mostly mean going into messy browser implementation details. This is something we could improve on, because the whys help a lot in understanding how stuff works. And perhaps it’s something we should improve on, because most of the reasons CSS is so bitched about today are related to misunderstanding how it works.
If you have been building things with CSS for a while, you will have come across a few WTF situations where you can’t for the life of you understand why a certain element insists on overlaying another, even though you’ve messed around with their z-indexes and used !important and even JavaScript can’t make things better.
So, stacking contexts.
A stacking context is an element - a plain html element - whose children can be ordered only relative to one another. By ordered I mean three-dimensionally, as in what goes on top of what. No element from outside can be layered between those children.
But this is all just words, so let’s look at some actual elements. And because most of what stacking contexts seem to actually do is cause problems, I’m going to start with a problem:
Here we have a pretty classic website layout, with a main content area and a sidebar. In the main area we have an element that when hovered will cause another to display, but currently it’s going under the sidebar. In this situation, you might be tempted to give the element a z-index with a really high value, but (spoiler alert) that won’t work. Why?
When something doesn’t work in CSS, it’s often a good idea to go and have a look at the rules applied to the parent elements.
In this case, the popup is a child of the main content area, which itself has a z-index, and it’s a lower one than the sidebar element has. So in relation to the sidebar, it’s as if popup had the z-index of main.
The z-index applied to main is generating a stacking context within that element. So main is now a context within which all its children are stacked. What this means is that the z-indexes of main’s children now only apply relative to each other, and not relative to any elements that are outside main.
If we remove the z-index from main, it will no longer be a stacking context so popup will now take its place within the root stacking context - and that means it’s now stacked in relation to the sidebar and its children too.
This kind of makes sense, because when you apply z-index you’re ordering an element relative to other elements on the page, and you’d expect the children of that element to maintain that order.
So this is what stacking contexts do. But that’s not the whole story.
Let’s look at some more examples of stacking contexts:
Here we have 3 squares: Pink, Yellow and Green, some of which have descendants. Notice how Pink Grandchild overlays Yellow, but is overlaid by Yellow Child, so they’re all interleaved. This tells us that none of the parent squares are stacking contexts.
Now let’s say you want to rotate Yellow by 90 degrees. In the demo above, uncomment the transform applied to .yellow
. When you add the transform to rotate it, Pink Grandchild now overlays both Yellow and Yellow Child. Yellow is now its own stacking context, so Pink Grandchild can no longer come between it and its descendants, but it overlays them because it has a z-index and Yellow doesn’t. The other thing to notice is that Yellow now overlays Green, and that is because elements that are stacking contexts will get rendered on top of elements that aren’t, even when they don’t have z-indexes.
Now let’s try changing the opacity of Pink. In the demo, you can uncomment the opacity property under .pink
to see what happens. It turns out opacity also creates a stacking context, and when you have two stacking contexts without z-indexes the one that comes later on in the markup wins - so Yellow now overlays the whole Pink family.
So the next question is: what properties can create stacking contexts?
Turns out there are quite a few. Let’s have a closer look at them:
z-index !== 'auto' &&
(position !== 'static' ||
parentElement.style.display === ('flex' || 'grid')
)
opacity !== 1
transform !== 'none'
mix-blend-mode !== 'normal'
filter !== 'none'
perspective !== 'none'
isolation === 'isolate'
position === 'fixed'
The first is probably the most well known: if z-index is other than auto and the element’s position is other than static or the parent’s display is set to flex or grid. So for elements inside a flex or a grid container, just setting z-index by itself will create a new stacking context.
Why this difference in behaviour? Digging through www-style@w3.org, I found that the need to position an element for z-index to take effect was not universally considered the right thing to do, and there was even some discussion as to whether it would be possible to change that altogether. As changing existing behaviour could break a lot of websites, it was decided to go with discarding the need for positioning only in the new layout models - flex and grid.
The others are more interesting as they don’t seem to be related to layering at all. Opacity, transform, mix-blend-mode, filter and perspective will sound very familiar to anyone who has used Photoshop or other image editing software; they are all related in some way to image manipulation.
In the CSS universe, they have something else in common: all are properties that are not inherited, yet affect the children of elements they are applied to. So setting opacity smaller than 1 on an element means all the element’s children will acquire the same degree of transparency as their parent. Rotating an element 90 degrees as we saw earlier means all its children will also be rotated 90 degrees.
Here we begin to approach the how and the why of stacking contexts: what the stacking context does is tell the browser that the element and its contents are a closed group, so, for rendering purposes, they can be flattened into a single layer. And this means that these properties can be applied only once to the whole lot, instead of being applied separately to the parent and each of its children. This simplifies implementation and improves performance.
What about the remaining properties?
Isolation is unique because its only function is precisely to create a stacking context. What is that useful for? Its main documented use case is together with mix-blend-mode (the isolation property arrived together with mix-blend-mode and background-blend-mode as part of the W3C Compositing and Blending spec).
Mix-blend-mode allows an element to blend with whichever elements are behind it, but only those elements that belong to the same stacking context as itself. So as well as establishing a stacking context for all the children of the element it is applied to, mix-blend-mode depends on the stacking context its own element belongs to as well. Let’s look at a practical example:
Here we have a Pink square which is empty, and a Green square with a Black child and an Orange grandchild.
There are no stacking contexts yet.
Now if we uncomment mix-blend-mode: overlay
on .black
, immediately not only Black changes but also its own child, Orange. And they are blending with both their parent Green and with its sibling Pink.
But if we add isolation: isolate
to .green
, now Black and Orange are only blending with Green and no longer with Pink.
So this is the main purpose of isolation for now.
Lastly on our list of stacking context-inducing properties we have position fixed.
The main reason for position fixed to create a stacking context is performance related. Mobile browsers started doing it even before it was in the spec to make scrolling smoother.
Why? If the fixed element is not a stacking context, and it has a few children that may have z-indexes, and other elements on the page that also have z-indexes are allowed to go between the fixed element’s children, there will be a lot of repainting going on as all those layers will have to be painted separately. So in making fixed position a stacking context it and all its children are flattened, so that whatever goes over or under it, only one fixed position layer needs a repaint.
The above demo shows a situation where layering changes depending on whether the “Mr Fixed” rectangle’s position is set to fixed or not. Try changing its position value to anything other than fixed and compare what happens. Depending what browser you’re on, the experience can be very janky.
There are a couple of other properties that create a stacking context: declaring will-change
with any of the properties mentioned above as a value, because it tells the browser that a stacking context-inducing property is going to change in the future, so it can go ahead and create the stacking context in preparation for that.
Lastly, there’s the non-standard -webkit-overflow-scrolling
property that creates a stacking context when set to touch
. (This property addresses momentum-based scrolling.)
And that is the gist of it: stacking contexts are caused by a variety of properties and the main reasons for their existence are performance concerns and ease of implementation by browsers. They are not always related to z-index or ordering; they pop up wherever it makes sense to have several elements all on the same layer for rendering purposes.