Skip to main content
Ben Nadel at the jQuery Conference 2011 (Cambridge, MA) with: Niall Kader
Ben Nadel at the jQuery Conference 2011 (Cambridge, MA) with: Niall Kader

Organizing My Application Layers Using Z-Index Stacking Contexts In CSS

By on
Tags:

A few years ago, the concept of a stacking context in CSS finally clicked for me. And, as I've continued to maintain the same Single-Page Application (SPA) over the last decade, my thinking about z-index and stacking contexts has continued to evolve. My current mindset is that I want to go into my existing SPA and wrap whole swaths of the DOM (Document Object Model) tree in elements that do nothing but define a z-index and a stacking context that traps all descendant elements within it. This way, no z-index in one "layer" can possibly interfere with a z-index in another layer.

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

Managing z-index values does not have to be challenging. In my current application, it's only challenging because z-index values were applied with a global mindset. Which means, when layering portions of the application, your mental modal has to contain the entire application. This is untenable.

By wrapping an entire layer of the application in an element that provides a trapping stacking context, z-index values no longer have to be globally compatible - they only have to make relative sense within the bounds of a particular layer. This is much easier to build and more maintainable in the long-term.

The main difference between this post and my previous post from 4-years ago is that this post also wraps all of the "body-level" elements in a stacking context as well. This is to trap the absurd z-index values that developers will inevitably apply to their HTML.

To see what I mean, here's a high-level overview of the document I am envisioning. Note that the immediately children of the <body> tag all have very simple z-index values that increment:

<!doctype html>
<html lang="en">
<head>
	<meta charset="utf-8" />
	<title>
		Organizing My Application Layers Using Z-Index Stacking Contexts In CSS
	</title>
	<style type="text/css">
		.stacking-context {
			/*
				In order to create a stacking context, we have to have to use a non-
				static layout for the trapping element.
			*/
			position: relative ;
		}

		.widget {
			background-color: #f0f0f0 ;
			border-radius: 4px 4px 4px 4px ;
			box-shadow: 0px 0px 7px 3px rgba( 0, 0, 0, 0.3 ) ;
			height: 80px ;
			padding: 20px 20px 20px 20px ;
			position: fixed ;
			width: 200px ;
		}
	</style>
</head>
<body>

	<!--
		Each stacking context below TRAPS all of its descendant elements in the same
		layer. It doesn't matter what z-index a descendant element uses at this point -
		it will never be able to escape the z-index defined at the stacking context.
	-->

	<!-- Main stacking context for all body-level elements. -->
	<div class="stacking-context stacking-context--body" style="z-index: 1 ;">

		<h1>
			Organizing My Application Layers Into Stacking Contexts In CSS
		</h1>

		<p>
			It turns out, layering an application over the long-term is challenging,
			especially when you start introducing 3rd-party widgets that add their own
			DOM elements (like Chat widgets and On-boarding widgets).
		</p>

	</div>

	<!--
		Stacking context for a "flyout widget". I would likely create a new stacking
		context for each unique "widget" that needs to be layered above the body.
	-->
	<div class="stacking-context stacking-context--flyout" style="z-index: 2 ;">
		<!--
			CAUTION: The z-index on this element (99999999999) won't interfere with the
			z-index of an element contained within a different stacking context.
		-->
		<span class="widget" style="top: 80px ; left: 80px ; z-index: 99999999999 ;">
			This is my fly-out widget.
		</span>
	</div>

	<!-- Stacking context for all modal windows. -->
	<div class="stacking-context stacking-context--modal" style="z-index: 3 ;">
		<!--
			CAUTION: The z-index on this element (55555555555) won't interfere with the
			z-index of an element contained within a different stacking context.
		-->
		<span class="widget" style="top: 140px ; left: 140px ; z-index: 55555555555 ;">
			This is my modal widget.
		</span>
	</div>

	<!-- Stacking context for all alerts. -->
	<div class="stacking-context stacking-context--alert" style="z-index: 4 ;">
		<!--
			CAUTION: The z-index on this element (22222222222) won't interfere with the
			z-index of an element contained within a different stacking context.
		-->
		<span class="widget" style="top: 200px ; left: 200px ; z-index: 22222222222 ;">
			This is my alert widget.
		</span>
	</div>

</body>
</html>

As you can see, all content within this application is contained within a .stacking-context element, even the content that is at the "static level" of document (ie, not "fixed" or "absolute" positioned). Because we have these large sections of the document, the z-index is quite simple; and it would be quite easy to change the existing order or add new layers in the future.

And, because each .stacking-context element traps all of its descendant elements, the z-index: 55555555555 node does not interfere with the z-index: 22222222222 node:

Application layers organized by z-index stacking context.

As you can see, the <body>-level stacking contexts prevent wild z-index values in one layer from colliding with z-index values in another layer. Essentially, each layer becomes a quarantine zone within which stacking contexts become locally relevant, not globally relevant.

Not only does this make my code easier to organize, it makes interfacing with 3rd-party JavaScript widgets (think Chat, think on-boarding) easier since we have more control over how elements are layered within the application. We can either stick them in a new top-level application layer; or, we can attach them to an existing layer and have confidence that we won't get unexpected user experiences.

The z-index property in CSS is awesome! But, without some sort of organizational technique, it can quickly lead to a chaotic arms race in which each developer is trying to add the next-highest z-index value on her widget. By using high-level stacking contexts, layering your application's user interface (UI) becomes much easier.

Architectural z-index Code Smells

It might not be obvious that you have a z-index stacking "problem". So, here are some "code smells" that might indicate a need for refactoring:

  • You have any z-index values that look like 999999999. This is a "hail Mary" technique that is only ever needed when you have no organization of layers at all. And, you're just doing whatever you can to get one element to show above another element.

  • You have a notes file somewhere that outlines the z-index values of the major parts of your application. This is only needed when the organization is inherently unstructured and relies on a Developer's attention to detail in order to work.

  • You use CSS pre-processor variables that hold z-index values, like @widgetZIndex: 500. This is only needed when z-index values aren't being "trapped" locally in a maintainable stacking context.

How do I know that these are "code smells"? Because I've done all these things while maintaining and application over the course of a decade. And, none of them work well.

Want to use code from this post? Check out the license.

Reader Comments

Post A Comment — I'd Love To Hear From You!

Post a Comment

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel