HTML & CSS
Article

Managing CSS Stacking Contexts in a “Hostile” Environment

By Thierry Koblentz

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.

TL:DR; ProposalDemo

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 (see demo). 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 (see “tooltips” versus “ad” in the demo).

When looking at the demo, 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 (i.e. the modal in the demo).

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

To better understand this issue, please visit this demo page where neither the right rail nor its modules are sandboxed. Scroll down the page a little and hover over the ad. You should see the ad overlapping the Header and the “button” for the modal peeking through the ad.

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

Comments
PaulOB

Interesting and well thought out article Thierry.

z-index has always been a pain to manage (especially in the early days when IE didn't understand the auto value). It also just became harder now that webkit pulls fixed positioned elements out of their current context (contrary to the specs).

To me it would have made more sense for z-index to be simpler and a higher or lower z-index wins out as required. Atomic stacking contexts often fail because you may want a graphical element of your design overlapping a number of columns yet you will still want a flyout or popup being on top of this no matter where it is and adjusting the z-index of the parent dynamically breaks this layout and indeed things like shadows on the elements/columns will be lost while the dynamic effect takes place.

Thierry

Hey, Paul, Thanks

To me it would have made more sense for z-index to be simpler and a higher or lower z-index wins out as required

I'm not sure that would solve the problem though because it would require to know all elements involved in the stack and to also know their position in that stack (as there'd be a single stack). data that I find difficult to gather in a multi-team environment or when dealing with third-party components. Also, since source order plays a role too, the only box we know for sure would be on top of the stack is the one with the highest z-index (upper limit) which is also last in the source code. Not sure in this case authors would have much control. I think the atomic nature at least helps in this regard. It's far from perfect, but it allows to have some control over how different elements stack on the page, regardless of their own z-index.

As a side note, since you mentioned position:fixed and the specs - the one that bothers me is the fact that fixed works the same as absolute inside elements that are "transformed".

Thanks for the feedback!

PaulOB

Yes that one has bitten a few people.

True, that would be awkward.

Maybe we needed to have both atomic and non atomic z-index values so we could control individuals or blocks as required. For dynamic elements like flyouts a value like "on-top" would be nice so that it goes on top of everything regardless smile.

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in Front-end, once a week, for free.