Managing CSS Stacking Contexts in a “Hostile” Environment
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.
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”:
- All main boxes on the page must be positioned and styled with a
z-index
other thanauto
. - Each of these boxes must be identified via a common class:
stacking-context
. - Those same boxes must have
data-zindex-max
anddata-zindex-top
attributes containing thez-index
values necessary to move a box up in the stack. - The class
inner-stack
is applied to the wrapper of any module susceptible to being styled with a highz-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)