My approach to using z-index
The last time I got straight to the point people seemed to appreciate it. So…
The short version
Categorise all uses of z-index as either local or global.
Local: elements that need to render on top of a sibling or nearby element
- Must be contained in a new stacking context
- Will rarely have a
z-index
greater than1
Global: elements that must render on top of elements elsewhere on the page
- z-index values must be declared as global variables in a central location
- There should generally be fewer than ten in a site
The long version
To set the scene, I’ll run through a simplified example that highlights some of the problems that z-index can solve, and cause. Here’s the codepen, if you’re curious.
And here’s a little drop down menu for profile-related things.
But you know what, that header doesn’t really ‘pop’. It needs more snazzy. I think I might give it a box-shadow
:
Wait, I gave it a shadow. Where’s the shadow? I want a shadow!
Oooh, I see, the div that comes after the header in the DOM has a background colour (white), and it’s sitting on top of the header’s shadow.
I’ll just give the header a z-index: 1
. But z-index
— in the immortal words of the CSS spec — “Applies to: positioned elements”. So I’ll need to add position: relative
too.
Behold, a header with a shadow.
Now, to check that this change didn’t break anything else. I’m pretty sure it won’t have broken anything else.
Bugger.
Frankly I was lucky it worked without one to start with. It’s almost like I wanted this to happen. Lemme just give it a z-index
of 1
.
Ah, that’s better. So far so easy.
Next, I want to create an effect so that when I hover over a product it will add a shadow so that I know I am hovering over a product.
Looks great. But oh no, if the following item in this list has a background, it covers the bottom shadow — the same problem I had with the header.
Guess who, to the res-cue…
It’s z-index!
I’ll just add z-index: 1
on hover (and ditch my desire to transition the shadow).
I think you’ve probably guessed what’s going to happen when I hover over a product while the profile menu is open.
I’m sick of this fiddling, and I’m a savvy web developer, so I’m going to give that bloody profile menu a z-index
of 99999
— so now I know it will always be on top of everything (because 99999 is famously the largest rational number).
And finally, at long last, I have everything stacking correctly.
I can now say with complete certainty that z-index
will never bother me again because I’m sure no changes to the interface will ever be required and that I’ll never want anything to stack above that drop down menu.
[eleven seconds later]
The CTO has read an article about security and told us that we need to sign users out after 7 minutes of inactivity.
So now somebody (not you) adds a full screen modal that sits on top of everything. But they only give it a z-index
of 9999
.
Only 9999
? What a schmuck face! That’s no match for the far-superior 99999
on the profile drop down!
And to make matters worse, no one tested the scenario of the timeout message appearing while the profile menu was open, so this gem makes it all the way into production. That’s the beauty of z-index bugs, they don’t show up in automated testing and can easily sneak past manual testing.
A question to go to sleep with: how is setting z-indexes of 999, 9999, 99999 logically different to 3, 4, 5? (A hypnagogic logic topic, if I must.)
Fixing this thing
So, how do the rules from the start of this post help solve these problems? First let’s take a look at the elements that we applied a z-index
to and work out if they fall into the ‘local’ or ‘global’ category.
- The full-screen modal is global
- The drop down menu is global
- The header could go either way, but I’ll say global
- The product list item is local
Global z-indexes
When it comes to global stacking, we care about how two elements stack relative to each other when they’re both on the screen at the same time. Since the relationship is important, the only sensible thing to do is to record the z-indexes for each of these elements in the same place.
For this, I’ll use Sass variables. In addition to the three above, I’m going to add in some ones I know I’ll need later, too.
If you’re not using Sass or CSS variables, maybe you’d like to create classes (note these will still only work on positioned elements).
You won’t need many global z-indexes, especially if you only allow one drop-down menu at a time, one tool-tip at a time, one full-screen modal at a time, etc. I think the most I’ve had in a site is 6. Maybe if you’re writing Google Sheets you’ll have 20.
I use the values 1, 2, 3… because that’s how I was taught to count. But I’ve been told I’m crazy for doing this.
There is an argument for using 10, 20, 30 instead. Something like: “if you add a new layer in the middle, you can make it 35, rather than have to shift everything else up by one”. To this I shrug and say ‘meh’.
Maybe you feel an urge to do 100, 200, 300, or even multiples of a thousand! But this is plain wrong. If you feel that having numbers further apart is ‘safer’ somehow, it probably means you don’t have confidence in your z-index setup yet. So why not just trust me and give 1, 2, 3, 4 a go, I promise you that two z-index values only need to be different by 1 to have an effect.
Local z-indexes
When I was talking about ‘global’ z-index, I was actually referring to setting z-index
on elements in the ‘root stacking context’ (I probably should have put that in the previous section).
But one document can have many stacking contexts. So, when I say ‘local’, I’m talking about setting the z-index of an element within a new stacking context. When an element creates a new stacking context, no child element will be able to render on top of elements elsewhere on the page.
Think of it as ‘scoping’ your z-indexes. (This might make it more confusing, but I like to think of it as turning a z-index of 999
into 0.999
.)
Here’s my product element that required z-index: 1
so that its shadow rendered on top of its next sibling.
And here’s my DOM for that list:
Do I want this hovered element to ever render above elements outside the product list? No, of course not. So, I can safely ‘scope’ this behaviour to the product list. In fancy words, I want the .product-list
element to create a new stacking context.
We’re going to do this with CSS, which of course means there’s three different ways to do it, defined in three different specifications. I’m going to go into all three in detail, even though you only need to know the first one.
#1 New stacking context with position & z-index
The CSS 2 spec informs us that “stacking contexts are generated by any positioned element (including relatively positioned elements) having a computed value of ‘z-index’ other than ‘auto’”.
So position: relative
and z-index: 0
will create a new stacking context for me. If you want to be kind to future developers, don’t do this without an explanation of why you’re doing this.
Perhaps a nicely-named mixin will be all the explanation you need.
Disclaimer, I only just thought of this approach while writing this post, so it could be seriously flawed in some way I haven’t considered. Up until now I have used…
#2 New stacking context with transform
Don’t like mixins and want a one-liner? As the CSS Transforms spec tells us, “any value other than none for the transform results in the creation of a stacking context”.
Neato. Thanks, CSS Transforms spec.
So transform: translate(0)
will do the trick.
But! You should know that there’s a side effect of using transform
that might bite you in the buttocks.
You may have been told that position: fixed
is relative to the viewport. This is not true.
(I only learned this relatively recently and was quite surprised. I am curious to know how many people are learning this for the first time right now. Would you do me a favour and highlight the above sentence if this is news to you?)
From the spec: “any value other than none for the transform also causes the element to become … a containing block for fixed positioned descendants”.
So if you do transform: translate(0)
to create a new stacking context, no child of .product-list
can ever be fixed relative to the viewport. Something to think about.
#3 New stacking context with opacity
From the CSS Color spec, “implementations must create a new stacking context for any element with opacity less than 1”.
So opacity: 0.999
will also create a new stacking context. This is a terrible idea for obvious reasons.
Once this new stacking context is created, you can be confident that nothing within that element can render on top of anything outside that element. This is a good thing.
I said at the top that typically these ‘local’ uses of z-index
will have a value of 1
. That’s not to say there’s anything wrong if you need to layer a few things on top of each other outside of the normal DOM order, I’ve just never come across a situation where I needed to do this.
Sandboxing third party code
Sometimes you might want to use a third party library in your UI. For example, if you want a headache.
You may chose a library that has nasty z-indexes (like 999, the devil’s z-index) and does not contain itself by creating a new stacking context. This means that the left/right arrows in your hot little jQuery carousel will sit on top of your full screen modals. I won’t name any names, because I’m a classy guy, but I’m describing “bx slider”.
On the plus side, it’s good that they didn’t use z-index: 100000
which would defeat even the seemingly-unbeatable 99999
. Perhaps we should all just use z-index: 2147483647
.
(There is something so fundamentally flawed with the logic of z-index: 999
that I don’t know what to write to convince people not to do it.)
Right, we’re going to ‘sandbox’ a third party library. This is nothing more than wrapping it in a div that creates a new stacking context. Something like this will do the trick:
This has been an amazingly long blog post considering I only had two points to make.
But I’m still probably missing something important. Weird edge cases, browser quirks, historical tidbits? Please, do share.
Adios.