CSS Stacking Contexts for Fun and Profit
Zachary Levine Mar 6
At work recently I came across a situation where we fullscreened an element within the DOM using the browser’s native fullscreen API. This is normally trivial, but this situation was a bit weird as the element we were fullscreening provided a map of a basketball court, with locations from which shots were made laid on top. When the user clicked one of these shot markers, we opened a modal on top that showed the details of the play as it happened and the relevant video footage.
When the shot map was not fullscreened, this worked fine, but when we opened the modal, we suddenly had a conflict between the stacking contexts (layout and display ordering) for the rendered DOM tree and the z-index for the modal.
I’ll spare you the product details, but the problem is that fullscreened items modify the browser’s stacking context. The result was that even though we added the modal as the first child element of the < body > tag, it did not matter what z-index we set for it, it would not show. It would always be hidden behind the fullscreened shot map, making it appear as if clicking the shot marker on the map suddenly had no effect. Even worse, this allowed the user to spawn multiple modals as they could now continue clicking the normally hidden shot markers.
This last problem was easily solved by only allowing one modal to be open at a time, but that (single) modal was still hidden, and so another solution was needed.
The root issue here is that even though the modal was at a higher level in the DOM than the shot map, the use of the fullscreen API created a new stacking context, and this stacking context took precedence over normal z-index usage. The modal still existed and was rendered, but invisibly, underneath the new stacking context.
The reasoning for this is that the stacking context added by using the fullscreen API creates a new context, which is added to the tree as the last item in the tree. CSS creates a painting order (which is an ordered set) that results from the stacking context, which is a tree. It does this using a post-order depth first traversal of the stacking context. I won’t go into what that means here (but you can read about CSS using it and the traversal, which I highly suggest), but put simply whatever is added to the tree (or appears in the tree) later according to this traversal will appear closer to the end of the ordered set (the painting order). This matters because CSS prioritizes items in the ordered set based on how close they are to the end, as it renders things in order. Appearing later means it is painted later, and more likely to appear “above” other elements.
As the stacking context for fullscreened items is added after properties like z-index, this results in a fullscreened element taking precedence. This makes sense given the purpose of the fullscreen API, but this is what de-prioritized our modals out of visibility.
Fortunately, there was an easy, non-hackish way to fix this. The solution takes advantage of the fact that while setting an element as fullscreen modifies the stacking order, sibling items to the fullscreened element follow the normal usage of z-index. This can be a little more complicated than it sounds as elements that need to be visible alongside/above the fullscreened element must be re-appended to the parent element of the fullscreened element. For our case this was not an issue as we were already appending the modal to the tree on the relevant actions after the fact, and could simply change where we appended the modal. In other situations, you would have to do this deliberately.