My approach to using z-index

The last time I got straight to the point people seemed to appreciate it. So…

The short version

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 than 1

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

colour scheme: seagreen, mintcream and honeydew

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.


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.

Pro-tip: use the Unicode triangle (▲) rather than faff about with weird CSS border triangle hacks.

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

  • 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

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.

The variables don’t need to be in ascending order of character count, but it looks neat

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

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.

Speaking of hover

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

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.




I like machine learning stuff.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store