Managing CSS Stacking Contexts in a “Hostile” Environment

Share this article

Note: This is a proposal I recently wrote for Yahoo. It has been edited for an external audience.
The order in which the rendering tree is painted onto the canvas is described in terms of stacking contexts. Things get complicated when authors are unfamiliar with the stacking contexts of a page or are simply oblivious to their state.

Interactive Advertising Bureau Guidelines for z-index

From the IAB’s perspective, things are pretty straightforward. They have a whole table specifying z-index ranges for almost all types of content, not just ads.
Z-Index Range Content Type Details
< 0 Background Elements
0 – 4,999 Main Content, Standard Ads Standard ad tags in place with regular content. Includes OBA Self Regulation Message (CLEAR Ad Notice)
5,000 – 1,999,999 Expanding Advertising The entire expandable ad unit should be set within this range
2,000,000 – 2,999,999 Floating Advertising Over The Page ads (OTP’s)
3,000,000 – 3,999,999 Pop-up Elements Chat windows, message notifications
4,000,000 – 4,999,999 Non-anchored Floating Elements Survey recruitment panels
5,000,000 – 5,999,999 Expanding Site Navigation Elements Drop down navigation, site warnings, etc. Only the expanding portion of navigation elements should be included on this level.
6,000,000+ Full-page Overlays Full-window Over-the-Page (OTP) ads and Between-the-Page ads IF they cover page content
Source: IAB Display Advertising Guidelines

How big can z-index be anyway?

The CSS specs do not mention an upper limit for z-index but there is a maximum value because of the type of variable used to store that value (a 32-bit signed integer); thus the limit in modern browsers is 2,147,483,647. Despite this, there are ad guidelines that recommend using 2,147,483,648. What happens when greater values are used, like this one I found in a page once?
<div id="fresco-thirdparty-tag" style="position: absolute;
z-index: 10000000000; display: none; top: 0; left: 0;
width: 970px; height: 250px;">
Modern browsers treat such values as the same as the upper limit. In other words, using 100,000,000,000 is the same as using 2,147,483,647.

Does any of this matter?

The short answer: no, it does not matter one bit. And that’s because stacking contexts are atomic:
Stacking contexts can contain further stacking contexts. A stacking context is atomic from the point of view of its parent stacking context; boxes in other stacking contexts may not come between any of its boxes. 9.9 Layered presentation
In other words, no z-index value can change the position of a box in relation to boxes outside of the stack it belongs to. This is why it is impossible to manage stacks in a predictable manner without knowing about the document tree and the stacking contexts of a page.

So what are z-index guidelines for?

What are those guidelines based on? Source order, as well as z-order of ancestors, play too large a part when it comes to painting the rendering tree – so simply setting z-index ranges cannot be a solution. For example, if you look at the dynamic demonstration of IAB Z-Index guidelines you’ll see that things work nicely because they were designed against a specific set of requirements; but in reality, this is a rather brittle construct as even simple changes could require rethinking most stacking contexts within that page. For one, imagine styling the “Header” of that demo page with position: fixed (so it stays at the top of the viewport as users scroll down); what z-index value should be used there? Because the header needs to show over the “300×250 Base Ad”, its z-index should be at least 2,000,000. But note that anything above that value makes the header compete with “Floating Ads” for which the z-index range is 2,000,000 - 2,999,999. Now imagine we have a search box above the left/right nav, with a drop-down à la “Search Assist“; what z-index
value should be used there? Because the drop-down would need to show over the nav, its z-index value should be at least 6,000,000. But note that anything above that value makes the dropdown compete with the “Full Page Overlay” for which the z-index range is 6,000,000+ (even if there is little chance that both show at once). Of course, there are also issues related to “state”. Any given box could require that it be positioned differently in the stack depending on its behavior or the behavior of surrounding boxes (dropdowns, flyouts, tooltips, expanding ads, etc.). In such cases, z-index values need to be updated on the fly – see demo further below.

A recipe for disaster

As the IAB demo shows, boxes that do not belong to a stack may be positioned anywhere in relation to other stacks. This is the reason why managing “granular” stacks necessitates centralizing z-index values and enforcing strict rules related to contexts – something that is bound to fail in a multi-team environment where developers may not have access to either the data or the tool (i.e. variables in a pre-processor file).

Proposal

This is about implementing a defensive mechanism to manage stacking contexts through a page. We need a solution that allows us to position a box in relation to other boxes within the same stack or even boxes outside the stack it belongs to.

Explicit contexts

The idea is to style the main boxes of a page to create an explicit “top-level” stack order. This leverages the atomic nature of stacking contexts and ensures containment at the highest level. We can then predict the behavior of nested boxes regardless of their own z-index values.

Dynamic stacks

Once we have top-level stacking contexts, moving a main box up and down the stack suffices to move its nested boxes through that stack as well. Allowing a change of stacking contexts is crucial because there are times when boxes need to be positioned differently within a stack. When looking at the Demo (From time to time, SitePoint removes years-old demos hosted on separate HTML pages. We do this to reduce the risk of outdated code with exposed vulnerabilities posing a risk to our users. Thank you for your understanding.), pay attention to the number that appears in the black circle at the top of the right rail. That number is the z-index value of the column. The value changes as users interact with elements inside the column. That change allows boxes to switch between stacking contexts (the boxes can move through different stacks).

Declarative switch

We rely on data-* attributes to expose z-index values needed to move any given box through the stack (see code example below). This way we can manage the ordering of boxes in the stack without knowing anything about the page itself (either its DOM tree or its stacking contexts). The same mechanism can be used to reset the z-index of a main box to allow its nested boxes to participate in the main stacking context.

More explicit contexts

Removing a box from the main stacking context allows its nested stacking contexts to become part of the higher stack. This means the z-index of these inner stacks dictate their position in regard to the ordering of the main boxes. For example, in the Demo (From time to time, SitePoint removes years-old demos hosted on separate HTML pages. We do this to reduce the risk of outdated code with exposed vulnerabilities posing a risk to our users. Thank you for your understanding.), when the modal is triggered, the removal of the right rail from the top-level stacking context allows the ad to compete with other main boxes on the page. In other words, if the ad had a z-index of 10+ it would appear on top of the Header
. Because of the risk associated with resetting the z-index of a main box, it is preferable to create explicit stacks for modules we suspect to be styled with a high z-index value. Those can be ads or other modules that page owners do not have control of. Sandboxing these elements prevents boxes from appearing anywhere on the “z” axis. This way, we can still control these boxes even though their ancestor is styled with z-index: auto. Both issues are due to identical z-index values (maximum value) that paint elements in the rendering tree according to their positioning in the markup. The Header shows behind the ad because it comes first. The modal button shows in front of the ad because it comes last.

Implementation

This solution requires sharing a simple setup and a common vocabulary across grids (when there are different grids involved as it would be the case for Yahoo sites for example – Finance, Sports, Home Page, Answers, etc.). The “4-step program”:
  1. All main boxes on the page must be positioned and styled with a z-index other than auto.
  2. Each of these boxes must be identified via a common class: stacking-context.
  3. Those same boxes must have data-zindex-max and data-zindex-top attributes containing the z-index values necessary to move a box up in the stack.
  4. The class inner-stack is applied to the wrapper of any module susceptible to being styled with a high z-index value.
For example:
<div id="right-rail" class="stacking-context"
data-zindex-max="5" data-zindex-top="10">
<div class="inner-stack">
<div id="fresco-thirdparty-tag"
style="position: absolute; z-index: 10000000000;">
data-zindex-max This attribute is used to declare the highest z-order the box is allowed to be styled with. For example, this could be a column meant to always show behind the Header. data-zindex-top This attribute is used to declare the highest z-order set within the page. For example, this would be the position of the Header inside the stack. Any element styled with that z-index value would appear in front of the Header (as long as that element comes later in the source code). .inner-stack This class is used to keep modules in check by preventing their own z-index to come into play. That z-index value is set via CSS by the page/grid owner.

Advantages of this solution

It reduces the risk of breakage By leveraging the atomic nature of stacking contexts, we can sandbox modules, making their z-index values irrelevant in relation to other boxes on the page. It is predictable No more guessing game; authors do not need to know anything about the document tree or its stacking contexts to be able to move boxes through stacks. It centralizes responsibilities Everything lies in the hands of the team responsible for the page itself – the team that knows everything about the document – its tree, its stacking context, etc.

Further thinking

In projects that use a single grid as well as in projects that share a common one, things could be made more declarative by using a preset of rules to swap z-index based on meaningful classes. For example:
  • .coverTheHeader {...}
  • .coverAllColumns {...}
  • .coverTheRightRail {...}
  • .coverTheLeftRail {...}
  • .coverTheMainContent {...}
  • etc.

Code Example

Below is the JavaScript used in the main demo page (From time to time, SitePoint removes years-old demos hosted on separate HTML pages. We do this to reduce the risk of outdated code with exposed vulnerabilities posing a risk to our users. Thank you for your understanding.). This script is responsible for switching the z-index value of the column that holds the ad and the “modal” button. It also “frees” these elements by resetting the z-index value set on their wrapper. It is the combination of both of these styles that allows these elements to behave as expected even though a very high z-index is applied to them.
(function(document) {
function findAncestor(el, cls) {
while ((el = el.parentElement) && !el.classList.contains(cls));
return el;
}

// constants
var AUTO = 'auto',
CSS_MODAL_ON = 'modal-on',
CSS_INNER_STACK = 'inner-stack',
DOC = document,
WIN = DOC.defaultView,
HTML = DOC.documentElement,

// elements
theAd = DOC.getElementById('ad'),
theAncestorStack = findAncestor(theAd, 'stacking-context'),
theInnerStack = DOC.getElementsByClassName(CSS_INNER_STACK)[0],
theModal = DOC.getElementById('modal'),
theStackUpdate = DOC.getElementById('stackUpdate'),

// globals
theInnerStackAncestor,
zIndexAncestor = WIN.getComputedStyle(theAncestorStack, null).zIndex,
zIndexCurrent, // will be assigned
zIndexInnerStack = WIN.getComputedStyle(theInnerStack, null).zIndex,
zIndexMax = theAncestorStack.getAttribute('data-zindex-max'),
zIndexTop = theAncestorStack.getAttribute('data-zindex-top');

function switchStackingContext (e) {
// we move the ancestor through the stack according to zIndexMax value (highest value allowed for that ancestor box)
if (WIN.getComputedStyle(theAncestorStack, null).zIndex === zIndexMax) {
theAncestorStack.style.zIndex = zIndexAncestor;
} else {
theAncestorStack.style.zIndex = zIndexMax;
}

// we get the ancestor that wraps boxes with potential high/crazy z-index
theInnerStackAncestor = findAncestor(e.target, CSS_INNER_STACK);

// we check for the actual value before we change it
zIndexCurrent = WIN.getComputedStyle(theInnerStackAncestor, null).zIndex;

// we reset the z-index from the parent ancestor allowing the box to move up (wherever it wants to)
if (zIndexCurrent !== AUTO) {
theInnerStackAncestor.style.zIndex = AUTO;
} else {
theInnerStackAncestor.style.zIndex = zIndexInnerStack;
}

// we update the value on the page
theStackUpdate.innerText = theAncestorStack.style.zIndex;
}

function resetStackingContext (e) {
// we reset the z-index of the ancestor to free the inner box
if (WIN.getComputedStyle(theAncestorStack, null).zIndex === AUTO) {
theAncestorStack.style.zIndex = zIndexAncestor;
} else {
theAncestorStack.style.zIndex = AUTO;
}

// we get the ancestor that wraps boxes with potential high/crazy z-index
theInnerStackAncestor = findAncestor(e.target, CSS_INNER_STACK);

// we check for the actual value before we change it
zIndexCurrent = WIN.getComputedStyle(theInnerStackAncestor, null).zIndex;

// we reset the z-index from the parent ancestor allowing the box to move up (wherever it wants to)
if (zIndexCurrent !== AUTO) {
theInnerStackAncestor.style.zIndex = AUTO;
} else {
theInnerStackAncestor.style.zIndex = zIndexInnerStack;
}

// we toggle this class to contextually style the modal (nothing to do with the solution per se)
HTML.classList.toggle(CSS_MODAL_ON);

// we update the value on the page
theStackUpdate.innerHTML = theAncestorStack.style.zIndex;
}

theAd.addEventListener('mouseenter', switchStackingContext, false);
theAd.addEventListener('mouseleave', switchStackingContext, false);
theModal.addEventListener('click', resetStackingContext, false);

}(document));
For reference, below are the two demo links referenced in the article. The first one (referenced multiple times in the article) is the one with the JavaScript, which “manages” the stack as users interact with the ad or the “modal”. The second one is the same thing without the script. It is included to show the issues we’d be facing if we were relying on third-parties to manage stacks.
  • Managing Stacking Contexts: Demo 1(From time to time, SitePoint removes years-old demos hosted on separate HTML pages. We do this to reduce the risk of outdated code with exposed vulnerabilities posing a risk to our users. Thank you for your understanding.)
  • Managing Stacking Contexts: Demo 2 (without the script)

Frequently Asked Questions about CSS Stacking Contexts

What is a stacking context in CSS?

A stacking context in CSS is a three-dimensional conceptualization of HTML elements along an imaginary z-axis relative to the user. It determines the stacking order of elements (which element would be in front and which would be at the back) when there is a potential overlap. The stacking context is created by specific CSS properties such as opacity, transform, z-index, and others. Understanding stacking contexts is crucial for managing the layout and visibility of different elements on a webpage.

How does the z-index property influence the stacking context?

The z-index property in CSS plays a significant role in determining the stacking context. It allows you to control the vertical stacking order of elements that overlap. The element with the highest z-index will be displayed on top. However, z-index only works on positioned elements, and it creates a new stacking context when applied with a value other than ‘auto’.

What are the properties that create a new stacking context?

Several CSS properties can create a new stacking context. These include: position (with a value of absolute, relative, or fixed), flex (with a value other than ‘none’), grid (with a value other than ‘none’), opacity (with a value less than 1), mix-blend-mode (with a value other than ‘normal’), and transform, filter, clip-path, mask/mask-image/mask-border (with a value other than ‘none’).

How does opacity affect the stacking context?

The opacity property in CSS can create a new stacking context. When an element has an opacity value less than 1, it forms a new stacking context. This means that the element and its children will be grouped together in terms of stacking order, separate from the parent context.

What is the role of the ‘isolation’ property in CSS?

The ‘isolation’ property in CSS is used to create a new stacking context. When set to ‘isolate’, it forms a new stacking context, isolating the element from its siblings. This can be useful in controlling the blending of overlapping elements.

How does the ‘transform’ property affect the stacking context?

The ‘transform’ property, when set to any value other than ‘none’, creates a new stacking context. This means that the transformed element and its children form a separate group for stacking purposes. This can be useful when you want to manipulate an element without affecting the stacking order of other elements.

How can I manage multiple stacking contexts on a single page?

Managing multiple stacking contexts on a single page requires careful planning and understanding of how different CSS properties affect the stacking order. You can use the z-index property to control the order of elements within a single stacking context. For multiple stacking contexts, you need to consider the properties that create new contexts and how they interact with each other.

What is the default stacking order of elements without a z-index value?

By default, elements without a z-index value stack in the order they appear in the HTML. Elements later in the HTML will appear on top of elements that appear earlier. However, this order can be altered by creating new stacking contexts or applying a z-index value.

How does the ‘position’ property affect the stacking context?

The ‘position’ property, when set to ‘absolute’, ‘relative’, or ‘fixed’, can create a new stacking context when used in conjunction with a z-index value other than ‘auto’. This allows you to control the stacking order of positioned elements independently from other elements.

Can I nest stacking contexts within each other?

Yes, stacking contexts can be nested within each other. Each new stacking context created by a CSS property forms a separate layer, which can contain other stacking contexts. This allows for complex layering and overlapping effects. However, it’s important to manage these carefully to avoid confusion and maintain control over your layout.

Thierry KoblentzThierry Koblentz
View Author

Thierry is a French front-end developer who works for Yahoo (Sunnyvale, Ca.).

css stackingcss zindexLouisLstacking contextsz-axiszindex
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week