A Better Solution for Managing z-index with Sass

Tweet

Lately we have seen quite a few articles about managing z-index with Sass:

While that last article from Chris Coyier certainly helped me get started with my solution, none of the previous articles succeeded, from a technical standpoint, in presenting a simple and easy to use method.

Hence this article. I will walk you through my very simple system, showing you how I built it and how you can use it in your projects today.

Why the Need to Manage z-index Values?

First let’s answer this question: why do we need to manage z-index with Sass? We don’t see countless articles popping up about managing padding with Sass, right? So why do it for z-index?

Well, it has to do with the fact that z-index is mostly misunderstood. Moreover, accepting a unitless value between -infinity and infinity gives you plenty of occasions to screw things up.

So rather than using values like 9999999 and yelling at your screen because you don’t understand what’s going on, it may be interesting to use an extra tool — a CSS preprocessor in this case — to help improve things.

What’s the Concept?

The concept here is mostly the same in all articles on this topic: Build a function that accepts a keyword as an argument, which is mapped to a value. This value is the z-index that will be printed out.

Jackie Balzer uses lists. Doug Avery uses a mix of lists and maps. Chris Coyier uses maps. Actually, Chris Coyier’s solution is really close to what I have ended up with, except it lacks this little extra that makes the whole system great to use.

Ultimately we’ll come to this:

.element {
  z-index: z("modal");
}

Why Use a Function and not a Mixin?

Doug Avery uses a mixin in his article. The major difference with a function is the way you use it:

// With a mixin
// Me no likey!
.element {
  @include z("modal");
}

While I understand this is really a matter of preference, I really tend to avoid using mixins when there is only a single CSS property being output. The function is a perfect use case for something like that.

Opening the Beast

So we need a function accepting a single argument. Feel free to call it anything; I went with layer.

@function z($layer) {
  // ... where the magic happens.
}

What is the function doing anyway? It will look for the given argument in a layers map to see if it is mapped to a z-index value. If it is, it returns the value, else it returns an error message. So we need a map.

$z-layers: (
  "goku":            9001,
  "shoryuken":       8000,
  "default":            1,
  "below":             -1,
  "bottomless-pit": -9000
);

Two things here:

  1. I like to have my configuration variables out of the mixins/functions using them, in a dedicated file (e.g. _config.scss). Feel free to move it inside the z() function it you prefer.
  2. You can add/remove/update as many keys/values as you want, those are just examples.

Now back to our function.

@function z($layer) {
  @return map-get($z-layers, $layer);
}

At this point, we haven’t done much more than syntactic sugar for map-get($z-layers, ...). That’s actually pretty cool, because typing this thing over and over can quickly become annoying.

If the key exists in the map, it will return the index value mapped to it. If the key has not been defined, then it will return null. Whenever a property has a null value, the Sass compiler doesn’t output it.

So in case you call for an unknown key, Sass will just fail silently, which is not ideal. Let’s improve this by using the @warn directive to alert the developer (that’s you) in case the key doesn’t exist in the map:

@function z($layer) {
  @if not map-has-key($z-layers, $layer) {
    @warn "No layer found for `#{$layer}` in $z-layers map. Property omitted.";
  }

  @return map-get($z-layers, $layer);
}

There. In case you ask for an undefined key (for instance “SitePoint”), Sass will print the following in the console:

“No layer found for SitePoint in $z-layers map. Property omitted.”

Pretty cool, right?

Playing with the New Toy

Now that we are done building our function, it’s time to play with it! As you saw at the beginning of this article, using the function is fairly straightforward:

.modal {
  // ...
  z-index: z("modal");
}

.modal-overlay {
  // ...
  z-index: z("modal") - 1;
}

The idea is to always use this function when defining z-index values. Using it only part of the time makes no sense and pretty much kills the system.

Also, I think it’s better to keep the layer map as light as possible. The more layers you add, the more you make your Z scale complex. Try finding a couple of recurring values, map them to generic keywords, and you should be good to go.

Pushing Things Further with Nested Contexts

You are probably not without knowing that z-index values are not absolute. They are all related to their own stacking context. That basically means that if you try to make an element from Context-A appear on top of an element from Context-B, but Context-B is drawn on top of Context-A, then even an index of over nine thousand would not be enough.

Note: For more information on stacking contexts and z-index be sure to read this tremendous article by Philip Walton.

Now if we want to make our system aware of stacking contexts, we could have nested maps. For instance, if our modal is initializing a new stacking context and we want to order elements in it, we could update the map:

$z-layers: (
  "goku":            9001, 
  "shoryuken":       8000,
  "modal": (
    "base":           500,
    "close":          100,
    "header":          50,
    "footer":          10
  ),
  "default":            1,
  "below":             -1,
  "bottomless-pit": -9000
);

Problem is, we cannot easily fetch a value from nested maps, not with map-get in any case. Fortunately, building such a function is easy:

@function map-deep-get($map, $keys...) {
  @each $key in $keys {
    $map: map-get($map, $key);
  }

  @return $map;
}

This is enough. Simple, isn’t it? Now, we can write our modal module like this:

// New stacking context
.modal {  
  position: absolute;
  z-index: z("modal", "base");

  .close-button {
    z-index: z("modal", "close");
  }

  header {
    z-index: z("modal", "header");
  }

  footer {
    z-index: z("modal", "footer");
  }
}

Which would yield the following result:

.modal {
  position: absolute;
  z-index: 500;
}

/* This is `100` in the modal stacking context */
.modal .close-button {
  z-index: 100;
}

/* This is `50` in the modal stacking context */
.modal header {
  z-index: 50;
}

/* This is `10` in the modal stacking context */
.modal footer {
  z-index: 10;
}

To the Future!

Some day in the bright future we all dream of, we will have native CSS variables. Then we won’t need all this anymore. Instead, this is what will happen:

:root {
  --z-goku:            9001;
  --z-shoryuken:       8000; 
  --z-modal:            500;
  --z-default:            1;
  --z-below:             -1;
  --z-bottomless-pit: -9000;
}

.modal {
  z-index: var(--z-modal);
}

See? Pretty much the same thing, except it’s var() instead of z() and --key instead of key. Slightly longer to type, and probably a little more chaotic since those are individual variables and not a map. But it does the job well while keeping it native.

Final Thoughts

I don’t know about you, but I think this is a beautiful method to handle z-index in a Sass project. Not only does it keep things organized with every z-index stored in the layers map, but it is quite easy to have slight variations with operators as we’ve seen with our last example.

That’s it. Here is a Sassmeister demo to get you started. No reason not to apply this in your projects starting today.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • http://hugogiraudel.com/ Hugo Giraudel
  • M S

    Too few syntaxes to rember.
    More is always better.

  • Designer023

    Hi,

    this is great. This make more sense than I thought it would :)
    Can you show me where I would call the map-deep-get in the z function? Does it need to be in an @if to detect if a key has a sub-map?

  • http://hugogiraudel.com/ Hugo Giraudel

    If you like to over-complicate things, I suppose it’s a valid solution. :)