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.

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

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.

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”.