Decision Layer elements deliberately with stacking contexts

accepted

Overlap elements predictably by creating new stacking contexts for z-index values.

Decision

When creating UI patterns with overlapping layers, we will avoid ambiguity by creating new stacking contexts and using z-index values as low as possible.

Context

Uncoordinated use of z-index and accidental stacking context creation can cause unpredictable UI layering and maintenance challenges. Our team needs a predictable, maintainable approach to visual stacking.

When styling layouts and components, occasionally the need arises to layer an element above (or below) another one using the CSS z-index property. This property orders relatively and absolutely positioned elements along the axis that extends perpendicularly out of the display toward and away from the user. This value, however, only compares elements within the same stacking context. While some frameworks create design tokens for various z-index values (e.g. Drupal's list of core z-indices), these values cannot work globally. They are only able to layer elements within a single stacking context.

Instead, our CSS will be authored to keep z-indices low, and to only increase values by ±1 as needed relative to its stacking context.

Consequences

  • Controlled Layering: When elements are meant to be layered independently of other elements in the DOM, especially when there is a need to isolate the stacking behavior of certain components (e.g., modals, tooltips, or dropdowns).
  • Prevention of Global Stacking Conflicts: To avoid unintended stacking of elements that share the same stacking context, we will apply a z-index on elements where the stacking order should be confined to a specific subtree of the document.
  • Improved Predictability: Creating new stacking contexts will help ensure the visual layering order is more predictable and easier to manage in complex layouts, where many components and elements may overlap.

Example

Consider a common design aesthetic in which a site has a header element, containing the site's main navigation, which has dropdown items. The markup for the page may look something like this:

<html>
  <body>
    <header>
      <nav>
        <!-- Dropdown menu -->
      </nav>
    <header>
    <main>
      <!-- Page Content -->
    <main>
  </body>
</html>

To create the dropdown layout, we set position: relative; on the top-level menu items and position: absolute; on the container of our second-level items. By default, elements display statically with an automatic z-index that is equivalent to 0. If we cause elements to overlap using a negative margin, elements that appear later in the DOM will cover elements declared earlier. Since our dropdown lists appear earlier in the DOM, they will default to appearing below the content in <main>.

If we opt to use a global z-index system, we might decide to simply apply an arbitrarily-high z-index value to our dropdown list. This will work fine for a while, but if a later feature adds another floating-type component (e.g. modals, tooltips, flyouts, dropbuttons, etc) then you've got to manage how those elements mesh together in a global system. Then, another change comes in that creates a new stacking context and your global system has broken and needs work-arounds.

If we instead opt to scope our z-index values to their compoents and nearby UI patterns, we can add the following CSS to our page layout:

nav {
  position: relative;
  z-index: 1;
}

main {
  position: relative;
  z-index: 0;
}

In doing so, we've created two new stacking contexts: one for navigation and another for the main content. Since our nav has the higher stacking context relative to main, all elements in <nav> will always appear above all elements in <main>. This specific example is not meant to be prescriptive - there will certainly be cases in which elements in main may need to appear above the navigation - but it illustrates the concept. We've created a new stacking context for the navigation and raised it by a manageable value of 1, as well as creating a new context for the rest of the page with a manageable value of 0. In this way, we can see the sibling contexts and know that one was deliberately placed over the other.

References