pwshub.com

What The Heck, z-index??

Introduction

In CSS, we're given a tool to explicitly control the stacking order of HTML elements: z-index. Elements with a higher value will appear on top:

Because .first.box has a larger z-index than .second.box, it stacks in front. If we remove that z-index declaration, it falls to the back. The code above is editable—give it a shot!

Things aren't always so simple, however. Sometimes, the larger z-index value doesn't win.

Check out what's going on here:

.tooltip has a much larger z-index than header! So why on earth is the header on top?

To unravel this mystery, we'll need to learn about stacking contexts, an obscure-yet-fundamental CSS mechanism. In this article, we'll explore what they are, how they work, and how we can use them to our advantage.

Link to this headingLayers and groups

If you've ever used image-editing software like Photoshop or Figma, you're probably familiar with the concept of layers:

3 layers in a Photoshop document: a cat photo (bottom), a moustache (mid), and a halo (top)

Our image has 3 separate canvases, stacked like pancakes. The bottom layer is a cat photo, with 2 layers on top that add silly details. By flattening these layers, we wind up with a final composition:

A cat photo with a poorly-drawn moustache and halo

In these programs, we can also group layers:

The 3 layers from the previous drawing are in a group, “Cat”. Another group, “Dog”, includes a top hat and a dog photo

Like files in a folder, a group allows us to segment our layers. In terms of stacking order, layers aren't allowed to “intermingle” between groups: All of dog's layers will appear on top of all of cat's layers.

When we export the composition, we don't see the cat at all, since it's behind the dog:

A dog photo with a poorly-drawn top hat

When it comes to CSS, things work in a similar way: elements are grouped into stacking contexts. When we give an element a z-index, that value is only compared against other elements in the same context. z-index values are not global.

By default, a plain HTML document will have a single stacking context that encompasses all nodes. But we can create additional contexts!

There are many ways to create stacking contexts, but here's the most common:

.some-element {
  position: relative;
  z-index: 1;
}

By combining these two declarations, a secret mechanism is triggered: a stacking context is created, forming a group around this element and all of its children.

Let's take another look at our problem from above:

<style>
  header {
    position: relative;
    z-index: 2;
  }
  .tooltip {
    position: absolute;
    z-index: 999999;
  }
  main {
    position: relative;
    z-index: 1;
  }
</style>

<header>
  My Cool Site
</header>
<main>
  <div class="tooltip">
    A tooltip
  </div>
  <p>Some main content</p>
</main>

We can map out the stacking contexts being created in this snippet:

  • The root context

    • <header>

    • <main>

      • <div class="tooltip">

Our .tooltip element has a z-index of 999999, but that value is only relevant within the <main> stacking context. It controls whether the tooltip shows up above or below the adjacent <p> tag, nothing more.

Meanwhile, in the parent context, <header> and <main> are compared. Because <main> has a smaller z-index, it shows up underneath <header>. All of its children come along for the ride.

Link to this headingFixing our example

How do we solve our tooltip problem? Well, in this case, we don't actually need to create a stacking context on our <main>:

Without a z-index, <main> won't create a stacking context. Our hierarchy, then, looks like this:

  • The root context

    • <header>

    • <div class="tooltip">

Because the header and our tooltip are now in the same context, their z-index values face off, and the tooltip emerges as the victor.

An important distinction: we're not talking about parent/child relationships here. It doesn't matter that the tooltip is more deeply nested than the header. The browser only cares about stacking contexts.

Link to this headingCreating stacking contexts

We've seen how we can create a stacking context by combining relative or absolute positioning with z-index, but it's not the only way! Here are some others:

  • Setting opacity to a value less than 1

  • Setting position to fixed or sticky (No z-index needed for these values!)

  • Applying a mix-blend-mode other than normal

  • Adding a z-index to a child inside a display: flex or display: grid container

  • Using transform, filter, clip-path, or perspective

  • Using will-change with a value like opacity or transform

  • Explicitly creating a context with isolation: isolate (More on this soon!)

There are a few other ways as well. You can find the full list on MDN(opens in new tab).

This can lead to some surprising situations. Check out what's happening here:

main doesn't set a z-index anymore, but it uses will-change, a property that can create a stacking context all on its own.

Link to this headingA common misconception about z-index

In order for z-index to work, we need to set position to something like relative or absolute, right?

Not quite. Check out what's happening here:

The second box is lifted above its siblings using z-index. There are no position declarations anywhere in the snippet, though!

I used to think that the z-index property had a hard dependency on the position property, that you had to set position to something like relative or absolute in order to use it. But that’s not quite right.

At its core, CSS is a collection of layout algorithms. There's Flexbox, Grid, Flow layout (the default), and many others. Each layout algorithm can implement each CSS property however it wants.

When we set position: relative or position: absolute, we switch to Positioned Layout, a layout algorithm that implements z-index. It's not the only one that does, though! Flexbox and Grid both implement it as well.

When we stop thinking of CSS as a big grab-bag of properties and start thinking of it as a constellation of layout algorithms, the language starts to make a lot more sense. You can learn more about this alternative mental model in my article, “The Importance of Learning CSS”.

Link to this headingHold on a minute…

There's a Weird Thing here, and I think it's worth pondering about for a minute or two.

In our Photoshop analogy, there is a clear distinction between groups and layers. All of the visual elements are layers, and groups can be conjured as structural helpers to contain them. They are distinct ideas.

On the web, however, the distinction is a bit less clear. Every element that uses z-index must also create a stacking context.

When we decide to give an element a z-index, our goal is typically to lift or lower that element above/below some other element in the parent stacking context. We aren't intending to produce a stacking context on that element! But it's important that we consider it.

When a stacking context is created, it “flattens” all of its descendants. Those children can still be rearranged internally, but we've essentially locked those children in.

Let's take another look at the markup from earlier:

<header>
  My Cool Site
</header>
<main>
  <div class="tooltip">
    A tooltip
  </div>
  <p>Some main content</p>
</main>

By default, HTML elements will be stacked according to their DOM order. Without any CSS interference, main will render on top of header.

We can lift header to the front by giving it a z-index, but not without flattening all of its children. This mechanism is what led to the bug we discussed earlier.

We shouldn't think of z-index purely as a way to change an element's order. We should also think of it as a way to form a group around that element's children. z-index won't work unless a group is formed.

Link to this headingAirtight abstractions with “isolation”

One of my favourite CSS properties is also one of the most obscure. I'd like to introduce you to the isolation property, a hidden gem in the language.

Here's how you'd use it:

.wrapper {
  isolation: isolate;
}

When we apply this declaration to an element, it does precisely 1 thing: it creates a new stacking context.

With so many different ways to create a stacking context, why do we need another one? Well, with every other method, stacking contexts are created implicitly, as the result of some other change. isolation creates a stacking context in the purest way possible:

  • No need to prescribe a z-index value

  • Can be used on statically-positionedA “static” element is one that doesn't set position to relative, absolute, fixed, or sticky. In other words, it doesn’t use Positioned Layout.elements

  • Doesn't affect the child's rendering in any way

This is incredibly useful, since it lets us "seal off" an element's children.

Let's look at an example. Recently, I built this neat envelope component. Hover or focus to see it open:

It consists of several layers:

I packaged this effect up in a React component, <Envelope>. It looks something like this (inline styles used for brevity):

function Envelope({ children }) {
  return (
    <div>
      <BackPane style={{ zIndex: 1 }} />
      <Letter style={{ zIndex: 3 }}>
        {children}
      </Letter>
      <Shell style={{ zIndex: 4 }} />
      <Flap style={{ zIndex: isOpen ? 2 : 5 }} />
    </div>
  )
}

(If you're wondering why Flap has a dynamic z-index, it's because it needs to shift behind the letter when the envelope is open.)

A good React component is sealed off from its environment, like a spacesuit. This spacesuit, however, has sprung a leak. Check out what happens when I use it near a <header> with z-index: 3:

Our <Envelope> component wraps the 4 layers in a div, but it doesn't create a stacking context. As a result, those layers can become “intertwined” with other components, like the world's most boring game of TwisterA party game involving coloured circles and tangled humans..

By using isolation: isolate on the top-level element within <Envelope>, we guarantee that it'll be positioned as a group:

function Envelope({ children }) {
  return (
    <div style={{ isolation: 'isolate' }}>
      <BackPane style={{ zIndex: 1 }} />
      <Letter style={{ zIndex: 3 }}>
        {children}
      </Letter>
      <Shell style={{ zIndex: 4 }} />
      <Flap style={{ zIndex: isOpen ? 2 : 5 }} />
    </div>
  )
}

Why not create a stacking context the old-fashioned way, with position: relative; z-index: 1? Well, React components are meant to be reusable; is 1 really the right z-index value for this component in all circumstances? The beauty of isolation is that it keeps our components unopinionated and flexible.

More and more, I'm starting to believe that z-index is an escape hatch, similar to !important. This is one trick that allows us to control stacking order without pulling the big red z-index lever.

Link to this headingDebugging stacking context issues

If you’ve ever struggled to get elements to stack in the correct order, you know how tricky it is to debug stacking context issues. Fortunately, I have a few tools that might help!

Microsoft Edge has an interesting “3D view(opens in new tab)” that allows us to view stacking contexts:

This view can be a bit overwhelming, especially in larger applications, but it can be a helpful way to understand the stacking contexts in our applications.

Let’s look at some other tools:

Screen recording showing that a z-index property is used incorrectly. The property is highlighted by this VSCode extension, and the developer has the option to either remove the property, or fix it by switching to positioned layout

This extension works on .css and .scss files, but not CSS-in-JS.

Screenshot of the Chrome devtools with a new pane that shows info about the element's current stacking context

Between all of these tools, you should be able to find something which can help you diagnose and fix z-index bugs. I've been using Andrea’s Chrome extension, and it's fantastic!

Link to this headingGoing deeper

Stacking contexts are a good example of how CSS is built on "hidden mechanisms". You can spend years building interfaces with CSS without knowing that they exist.

Unless you explicitly take the time to learn about these mechanisms, your mental model will always be missing pieces. And if your mental model is even slightly misaligned, it's only a matter of time until that discrepancy causes problems.

CSS doesn't have warnings or error messages. When something surprising happens, there's no clear "next step" to figure out what went wrong. These disruptions take us out of flow state and shake our confidence. I think this is why so many front-end developers don't enjoy writing CSS.

Once you build up an intuition for the language, though, CSS becomes an absolute joy. I love writing CSS nowadays.

I want to help other developers discover this joy. I've created a comprehensive self-paced online course that explains how CSS works at a deeper level, and teaches the practical skills I use every day to build all kinds of user interfaces.

It's called “CSS for JavaScript Developers”(opens in new tab), and it's available now. 😄

Last updated on

September 13th, 2024

# of hits

Source: joshwcomeau.com

Related stories
5 days ago - I have a set of baseline CSS styles that come with me from project to project. In the past, I'd use a typical CSS reset, but times have changed, and I believe I have a better set of global styles!
5 days ago - Every action we take on the web starts with a button click, and yet most buttons are ho-hum and uninspired. In this tutorial, we'll build an animated 3D button with HTML and CSS that sparks joy.
1 week ago - This blog has been around for a while (twenty one years currently) so it isn't too uncommon for me to revisit old topics and demos and rebuild them. I think today's post may be something of an outlier though. Way back in 2010, early 2010,...
3 weeks ago - Just like the Moon is constantly influencing planet Earth's ocean tides, it is likely that, at some point, Lua influenced your navigation over the internet. Today, we'll embark on an adventure to uncover how this well-rounded scripting...
2 weeks ago - As I continue to play with, and really freaking enjoy Leaflet, I thought it would be interesting to show a demo of using CSV data with it. This also coincides with an interesting dataset I got from the Data is Plural newsletter, a...
Other stories
2 hours ago - Ubuntu 24.10 ‘Oracular Oriole’ is released on October 13th, and as you’d expect from a new version of Ubuntu, it’s packed with new features. As a short-term release, Ubuntu 24.10 gets 9 months of ongoing updates, security patches, and...
3 hours ago - Did you know that CSS can play a significant role in web accessibility? While CSS primarily handles the visual presentation of a webpage, when you use it properly it can enhance the user’s experience and improve accessibility. In this...
4 hours ago - Design thinking workshops are your key to turning big problems into clear solutions. In this blog, I share how to run them efficiently and keep your team aligned. The post How to run a design thinking workshop appeared first on LogRocket...
5 hours ago - New memory-optimized X8g instances offer up to 3 TiB DDR5 memory, 192 vCPUs, and 50 Gbps network bandwidth, designed for memory-intensive workloads like databases, analytics, and caching with unparalleled price/performance and efficiency.
5 hours ago - Gain indispensable data engineering expertise through a hands-on specialization by DeepLearning.AI and AWS. This professional certificate covers ingestion, storage, querying, modeling, and more.