TL;DR
An element with
position:fixed
is positioned relative to the document (the viewport) which acts as its containing block.But, it will NOT always be relative to the document. When any element has
transform
,filter
orperspective
property, it acts as a containing block for all its descendants, including the elements whose position is set to fixed.
Now, the long story...
While I was working on enhancing the navigation bar of the
Polygon Wallet, I found myself stuck with a strange error. I spent almost a day trying to figure out the issue, consulted with the senior frontend engineers and also thought of making a separate Vue component as a workaround.
When I finally found the bug, I wouldn't shy away to say that my ego went crashing down, as it was a CSS bug, and my CSS skills are something I take pride in.
I was doing some changes to the code to align the dropdown menus to the cente of the navigation item (as shown in the picture below) for which I had to move the dropdown component inside the <li>
. Doing this would allow me to use the CSS position and transform properties to align the dropdown exactly at the bottom-center of the navigation item.
Till now, everything was working fine and nothing was unusual and unexpected.
Inside the dropdown menu, there were a bunch of notification items, which when clicked would open the modal component.
The components were nested something like this
<li>
<span>Notifications</span>
<dropdown-menu>
<item>
<modal />
</item>
<item>
<modal />
</item>
</dropdown-menu>
</li>
The modal component, which was supposed to open at the center of the screen when clicked on the item, would just behave weirdly and open inside the dropdown menu itself. I was sure that, any element with a fixed position would ignore all its ancestors and position itself relative to the document (the viewport or the root element). But that just didn't happen. The ancestor element, in this case, the dropdown menu was acting as a containing block for the modal component.
And as you might have guessed, it was due to the transform property. Any element with transform
, filter
or perspective
property will act as a containing block to all its descendants, including the elements whose position is set to fixed. Hence the modal, instead of positioning itself with respect to the document, positioned itself relative to the dropdown-menu component where I had used the transform property to align it exactly at the bottom-center of the navigation item. See CSS Transform Spec
How did I fix it?
Well, there is no fix for that, I just removed the transform property, gave a fixed width to all the dropdown components and achieved the same result with the help of the margin and position properties. But this uncanny relationship between position and transform property reminded me why CSS takes the crown as my favorite language even when I have a multi-paradigm language like JavaScript in my stack. β€
Thank you!
If you want to learn about CSS positions, refer to
CSS Tricks
MDN Web Docs
Top comments (8)
Really helpful! π
I'm glad, Shridhar.
If you work with React, you can use a Portal
Thank you! I can confirm for anyone else who finds this article while experiencing this CSS bug that React Portals solve the issue. The current doc for it is react.dev/reference/react-dom/crea.... Very easy to implement if you are already up to speed with React in general, and it solves the bug.
@peresnegro vue already have Teleport
Thank you so much for this article, @salilnaik! I spent about 15 minutes dealing with this bug and was just starting to enter the beginning stages of ripping my hair out over it and had just deduced that the transform property being set on the parent element was causing the issue, but thankfully I thought to search "element position fixed within element position absolute transform translate" and your post was on the first page of DuckDuckGo, and it confirmed what I was experiencing.
Unfortunately, in my case, using something other than transform: translate(y,x) on the parent element was not a possibility, but in the comments section, @porkopek mentioned using React Portals to work around the issue, which solved the problem for me!
And, based on the another commenter, it looks like there is also a similar tool in the Vue framework. I don't know Vue so can't confirm/deny on that one.
You might consider updating your article to reflect the two possible alternate solutions to the problem using React or Vue, as some people might not make it all the way down to the comments section in search of a solution.
Anyway, thanks again for this post! :)
Nice explanation! π
Very useful! Faced the same issue with a modal rendered in a list