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