Skip to main content
Light Dark System

Top Layer Popup Controller

Et2TopLayerPopupController Controller

Overview

Keeps hoisted Shoelace popups (select dropdowns, tooltips, date pickers, …) anchored to their widget when they open inside a CSS container query container.

A layout that can collapse its own columns has to know how wide it is, so it declares itself a query container with container-type: inline-size. That is where the trouble starts. By the letter of the spec a query container applies layout containment, which makes the element a containing block for position: fixed descendants - but no browser actually does that part. Measured on Chrome 152 and Firefox 142, a fixed descendant of a query container still resolves against the viewport, while contain: layout and transform on the same element both do capture it. Floating UI, which places every Shoelace popup, believes the spec: it takes the first container-type ancestor as the containing block and hands the popup coordinates measured from that element’s scrolled padding box. The browser then reads the very same numbers as viewport coordinates, so the popup lands container.scrollTop - container.top away from the field it belongs to - already wrong when nothing is scrolled, and drifting further with every pixel scrolled. Floating UI also treats the container as a clipping ancestor, squeezing a dropdown in a short tab panel down to a few rows.

Promoting the popup into the top layer settles the argument instead of taking a side: Floating UI short-circuits its containing block search for a top layer element and measures from the viewport, which is exactly where the browser paints it. Clipping ancestors drop out at the same time, so a dropdown may again overflow the panel it lives in. Crucially the container itself is left alone - forcing real containment on it (contain: layout) would fix these popups by breaking everything else that uses position: fixed to escape a scrolling panel, the rich text editor’s menus among them.

Only popups that asked to escape are touched: hoist gives a popup position: fixed, and a popup without it is deliberately placed against an offset parent inside the container and has to stay there. A popup with no query container above it is left alone too, since it is already placed correctly and clipped the way its widget intended.