When I first heard about the z-index
css property it sounds like such a helpful and innocent concept. But after some
years of using them I would declare them the
billion dollar mistake of web development. But let me explain that
in a bit more detail.
Some history
Back in the days using z-index
was the way to go if you wanted to make sure that some of your HTML content was
displayed on top of your other content. The main use case for that would be any kind of overlay or dialog, which tries
to get the user's attention. Often these components also disallow to click anywhere outside themselves, so that the user
has to take care of them. Since this is a very aggressive way of gaining attention, these elements were used very rarely
on websites. Maybe there was a single overlay on the entire website to get your user to sign up for your newsletter (not
that I've been a big fan of those...) or an overlay to show an image gallery.
Since the use cases were so limited, it was not a big deal to handle them. An implementation of such an overlay might
have looked something like this:
<!DOCTYPE html>
<html>
<head>
<style>
.overlay {
width: 300px;
height: 200px;
background: lightgray;
position: absolute;
z-index: 10;
top: 30px;
left: 100px;
font-size: 0.5em;
}
</style>
</head>
<body>
<h1>My headline</h1>
<h2>My subheadline</h2>
<div class="overlay">
<h2>My overlay content</h2>
</div>
</body>
</html>
You can check out this codepen to see the results.
Of course I am talking about pre-HTML5 times here, when you had to add text/css
as a type to your style
tag and use
a much more complex DOCTYPE
I was too lazy to look up now. Actually the above example would not even need the
z-index
definition, but it was still used a lot in cases like this, because some other component might already have
had a z-index
applied.
But the point remains: Putting an overlay like this on your website was not much of a big deal, also because the amount
of other elements using a z-index
was manageable.. But then something interesting happened: The web was getting more
and more interactive. Companies decided to use the web as a plattform for applications they have previously built as
desktop software. And software like this is a lot more complex than a website or even some documents CSS was originally
designed to style. And this is when it started to get messy. Until today I see CSS containing properties like the
following:
.overlay {
z-index: 99999 !important;
}
Basically it was just putting higher numbers to your z-index, to make sure nothing else was on top of your overlay.
At least until you wanted to place something on top of that overlay, in which case you just came up with an even higher
number. And I think you can imagine that this does not scale very well.
So since then z-index
was probably the css property I was most obsessed about. It just seems so unbelievably hard to
understand this concept. One of the biggest problems is probably that these values are compared among each other, and if
there are loads of them it easily gets too complex to manage. I did some research on how to tame that complexity, and
stumbled upon a few interesting approaches.
Draw inspiration from game development
In a CSS-Tricks article I have encountered one of these
interesting approaches. They use an idea which seems to be very popular in game development, which is putting all used
z-index
values in a single file. Since not all browsers
(looking at you, IE11) support the the new
CSS custom properties you usually have to use some kind of
preprocessor like SCSS to implement that.
$zindexHeader: 1000;
$zindexOverlay: 10000;
There are two interesting points to make about this approach:
- Leaving some space between the numbers allows for easy addition of more
z-index
values later. - Having all values defined enables developers to be aware of all
z-index
values.
However, in order for that work, you have to make sure that there is no z-index
property in your code base, which is
not using a variable of this file.
This article also contains a comment with a nice variant of
this approach. Instead of defining values using numbers, it makes use of SCSS lists and a function retrieving the index
of the given value from that list. The advantage is that you don't even have to think about any numbers anymore, and
inserting a new value is as easy as placing it correctly in the list. This sounds awesome, right?
Introducing stacking contexts
The thing is that even if you would be strictly following this rule, there might still be situations with an unexpected
outcome. That is because of the
stacking context
defined in the HTML specification. Browsers will apply the z-index
values only within the stacking context the element
was defined. Let's see an example to make it easier understand what that means.
<!DOCTYPE html>
<html>
<head>
<style>
.box-1, .box-2, .box-3 {
position: absolute;
width: 100px;
height: 100px;
}
.box-1 {
top: 20px;
left: 20px;
z-index: 1;
background-color: blue;
}
.box-2 {
top: 20px;
left: 20px;
z-index: 3;
background-color: red;
}
.box-3 {
top: 60px;
left: 60px;
z-index: 2;
background-color: green;
}
</style>
</head>
<body>
<div class="box-1">
<div class="box-2"></div>
</div>
<div class="box-3"></div>
</body>
</html>
If you are reading this without any knowledge about stacking contexts, you would probably say that the red box with the
class box-2
should be appearing on the very front. In case you thought so, you can have a look at
this codepen for prove that this is really not the case.
The reason for this behavior is that the blue box also has a z-index
value. Its value of 1
is lower then the value
2
of the green box, so the browser ensures that the blue box --- including its content --- will stay below the
green box. So the z-index
value of 3
of the red box will only be compared to other child elements of the blue box.
Philip Walton did a great job at explaining this in more detail.
And this somehow makes sense, because that might be what you want when you are only comparing box-1
and box-3
. The
developer writing the rules for these two elements probably wanted to ensure that box-3
is on top of box-1
,
independant of their children.
Unfortunately it is very likely that this contradicts with the behavior the developer implementing the overlay would
expect. I assume that the reason people are adding such high z-index
values, is that they want to be a 100% sure that
the elements are always visible. Too bad that the highest value doesn't help, if the element is part of a stacking
context. But it gets even worse: A new stacking context is not only introduced when an element has a z-index
attached,
but also with a few other properties (see the
full list on MDN).
So even if whatever you have build is currently working, other developers might break it by adding any of these
properties to any ancestor of your element without even realizing it (I can't blame them, they might not even know there
is an element like this in the subtree).
TL;DR: z-index
and stacking contexts are very complex.
These problems might not even occur on a website written using plain JavaScript. Just put the overlays right before the
closing body
tag, and they will appear on top of everything else. At least if there is no other z-index
set
somewhere, because the browser will render elements appearing later in the HTML source on top of elements that appear
sooner. That part actually sounds quite easy, right?
The problem is that with libraries endorsing thinking in components (like e.g React) this is not
that easy to achieve. That's because a component somewhere deep in the component tree might want to render an overlay,
that should appear on top of all the other elements, no matter where it is located in the source code. Let's assume your
application has a structure like this:
- App
Header
-
Form
Input
Textarea
- Submit Button
- Confirmation Dialog
I guess it would not be uncommon for the Header
and Form
component to have some z-index
value, to ensure that the
Header
will be display in front of the Form
component. If the form is now rendering a dialog to confirm e.g. storing
the provided information, it is not possible to display this dialog in front of the Header
, if the component structure
is repesented in the same way in the DOM.
But let's assume that no other z-index
--- or any property creating a new stacking context --- is used in the
application. Even then you are running into problems, because in React you might want to implement a single Overlay
component, that can be reused in multiple places. If you are displaying multiple of them, it might also be tricky to
display the correct one in front of the other. That's because the Overlay
component has always the same z-index
value. If you are relying on the z-index
for this kind of stuff, you would probably have to pass a z-index
prop to
your React components. And that feels as if we are doing the full circle and go back to where we've started: Trying to
find a higher number than everybody else. But fortunately the blog post is not finished yet.
Where to go from here?
I feel like I have just ranted a lot until now, but I think it is important to understand what lead me to the decision I
am going to explain next. I was facing with two different z-index
issues when working on Sulu's
administration interface within a very short amount of time. So I've decided to put a bit more effort into this. I took
inspiration from the classic JavaScript: The Good Parts. It has been
quite some time since I've read this book, and I know that JavaScript changed a lot rendering some of the advices in
this book obsolete, but I still like its general idea. Getting rid of stuff that is causing troubles. So that was
exactly what I did:
I removed (almost) all z-index
values from Sulu's codebase. Might seem a bit
radical, but I am sure this will pay off in the long run.
You might think that your special use case requires z-index
in order to make everything work, but we are building a
relatively complex single page application, and I was able to get rid of all z-index
properties. I think what I did
can be broken into two different tasks.
At first I have to say that it is really incredible how many z-index
properties you can avoid by just correctly
order your elements in the DOM. You want to make sure that some kind of elements to edit an image appear on top of the
image? Try to just put these elements after the image in your HTML source, and it will already work! No z-index
at all
is required for that, and you will avoid that any of the above reasons might break your application. I think that was
the most important realization when trying to avoid z-index
es.
There was only one element that was not that easy to transform using this advice: The header with the always visible
toolbar in our administration interface. The problem is that it appears at the very top and putting it first in HTML is
one way of achieving that. But if the header comes first, I would have had to add a z-index
to let it appear in front
of the content that comes after it. I then tried to move the header at the end of that container, but then it appeared
on the bottom of it (and I didn't want to start using position: absolute
or something like that, since that comes with
its own set of problems, which would probably fill a separate blogpost). For a short time I thought it does not matter,
but then I realized that the header has a box-shadow, that is hidden behind elements that are coming below the header in
HTML.
<!DOCTYPE html>
<html>
<head>
<style>
* {
margin: 0;
padding: 0;
}
header {
height: 20vh;
background-color: steelblue;
box-shadow: 0 1vh 1vh teal;
}
main {
height: 80vh;
background-color: ivory;
}
</style>
</head>
<body>
<header></header>
<main></main>
</body>
</html>
This codepen shows that the teal box-shadow
of the header is not visible.
Very tempting to use a z-index
...
The solution I came up with was to put the header at the end of the container, and use flex-direction: column-reverse
to the container element. That makes the header element appear on top of the others because it shows up later in the
source code, but still was display on the top of the screen, because flexbox reverses the order of the elements. I've
tried something similar with the order
CSS property, but without any luck. I am not totally sure how this approach
impacts accessibility, but I guess fiddeling around with z-index
values does also not help a lot with that.
<!DOCTYPE html>
<html>
<head>
<style>
* {
margin: 0;
padding: 0;
}
body {
display: flex;
flex-direction: column-reverse;
}
header {
height: 20vh;
background-color: steelblue;
box-shadow: 0 1vh 1vh teal;
}
main {
height: 80vh;
background-color: ivory;
}
</style>
</head>
<body>
<main>
</main>
<header>
</header>
</body>
</html>
This codepen shows how changing the order of the elements and use
column-reverse
also does the trick without needing a z-index
.
Now the only remaining open question is how a component like an overlay fits into that equation, especially if a
component deep in the component tree wants to render it. Actually
React has a built-in concept called portals helping exactly with that. Portals
allow to break out of the DOM of the parent component. This also fixes other problems with overlays, like the fact that
if the parent has overflow
set to hidden
it is not possible to display anything outside the parent container. We use
portals quite a lot, and append stuff at the end of the body tag. Since the elements are now rendered at the very end of
the body
tag --- and we don't have any other z-index
set --- setting a z-index
is not necessary at all! The only
drawback I ran into was that the order in the DOM seems to be dependant on when the portal code is called. That lead to
a confusing situation I was able to fix quite fast, and it didn't make any problems anymore. Still feels like something
that is good to know and might help debugging situations like this.
Last note: I mentioned that I removed only almost all z-index
. Some are still necessary, but only because of some
third party libraries making use of z-index
. The solution there was to set a z-index
of 0
on my own on a parent
container of these libraries. That introduced a new stacking context, which ensures that these elements of these
libraries are not shown in front of e.g. our overlays. But "not relying on z-index
" still made it to my criteria list
for evaluating libraries.
Finally I want to say: Whenever you are about to add a z-index
--- think very hard if there is really no better
solution.
Top comments (4)
Putting header below main and using reversed flex direction could have accessibility implications.
Just put ::after pseudo-element on header and add your shadow there.
Couple more lines of code but better solution overall...I think...
I was not sure about the accessibility implications... I also thought I tried the
::after
pseudo element, but it did not work as expected, because the below element will still be over the pseudo element. But not totally sure about that anymore, maybe I am going to try at one point again.Here is edited codepen
Okay, I also already made bad experiences with
position: absolute
, so I tried to not use that one as well.