Finding a Chrome bug about stacking contexts and z-index

I won't dive into definition of stacking contexts. There are already many resourses about the concept. I will talk about a bug that broke an app. And the steps I did in the proccess to find the issue.

So, I have a z-index problem, which often are easy to fix, until they are not.

The problem: bigger z-index is displayed under the lower z-index.

The context

A header and a modal positioned fixed. Both are web components and both positioned elements are inside of their shadow root.

Comparison with stacking context bug before and after

At first sight, I thought it will be easy. There's one thing that could happen - another stacking context that affects the modal component. The code would look like this. A very simplified version of it (without parents and other elements).

<my-header>
  #shadow-root
  <div style="position: fixed; z-index: 10;">
      My Header
  </div>
</my-header>

<my-modal>
  #shadow-root
  <html>
    <head></head>
    <body>
      <div style="position: fixed; z-index: 11;">
        My Modal
      </div>
    </body>
  </html>
</my-modal>

Let's debug

I checked all the parents of the modal, expecting to find another element positioned or using another CSS property that creates a new stacking context.

There was none.

I double checked the docs for other scenarios - nothing. I tried to Google different scenarios for this kind of issue. All results had more or less common causes - the ones that I expected to find in my case. I even asked ChatGPT for my problem - it gave me correct answers, but got lost at the end.

Asking ChatGPT about stacking contexts

Even weird was the fact that I saw the code working fine a few weeks ago. I was like: what the hell is going on? 🤬 Am I blind? Where is the god damn stacking context.

Next, I installed Stacking Contexts extension - nothing new, didn't detect any new stacking context. I test it in Safari - it worked. That was strange, 'cause usually it's the other way around. Firefox - it worked as well.

I checked another component that was similar with the header component - all good. So, there must be something different with this component. I compared them and the only difference is the <html> element.

<my-modal>
  #shadow-root
  <html> <!-- HERE IS THE DIFFERENCE -->
    ...
  </html>
</my-modal>

Back to MDN docs.

A stacking context is formed, anywhere in the document, by any element in the following scenarios:

  • Root element of the document (<html>).

The first scenario seems to be my scenario. The definition doesn't describe exactly my case, but the shadow root is a DocumentFragment that can be threated as a root element, I guess?

I made a quick test and replaced the <html> tag with a regular <div> through DevTools. It worked. Ok, so I found the problem.

Why now? Why only Chrome?

I kept thinking on the fact that the code worked a few weeks back. I checked my Chrome version - 111.0.5563.65. I found a colleague with Chrome 110 and checked the code - it worked for him.

Good. I narrowed down the problem - something changed in Chrome. In my mind the explanation was like this - the specs refers to any root element, including shadow roots. All the browsers missed it and Chrome fixed it now. It sounded a bit exaggerated, so I wanted to make sure.

I tried to find this detail in the official specs, but I got lost 😵‍💫. I checked the changelog for Chrome 111 and I found something related with stacking context. From this commit I saw the related bug. It was opened by Jake Archibald for View Transitions API. I knew from Twitter that Jake worked a lot on this API. I also read that the API adds a new rule for the stacking context mechanism.

Hmm. Is this a real bug in Chrome? I asked Jake on Twitter and he confirmed it's a bug that will be fixed (thanks Jake, for the clarification 🍺).

Before I finished this article, a Chrome bug was opened and also fixed 🤯.

Joke aside, the bug was opened later after I tagged Jake on Twitter - not sure if I was the first one to report it.

Solution

Most likely you don't need a workaround, because by the time you're reading this, Chrome already published another few versions with this bug fixed. But, if you still need it, here it is:

// Make sure you select the html element causing the issue, not the actual root element
html {
  view-transition-name: none;
}

I didn't find the most critical bug, but it was a good exercise of digging for the root cause.

I love these kind of debugging sessions, I learn a lot, but until the next one, see ya! 😉