← Back to Examples

📐 Positioning Edge Cases

How the dropdown anchors across CSS containing-block scenarios, and what the library does when it can't position the panel correctly on its own.

1. Baseline (no special ancestor CSS)

The simplest case: no ancestor establishes a containing block for fixed positioning. The dropdown anchors to the viewport and sits under the input.

wrapper CSS: (none)

2. Ancestor with <code>transform</code> (works correctly)

transform, perspective, filter, backdrop-filter, and qualifying will-change values establish a containing block for fixed-positioned descendants in every browser. Floating UI detects them, the browser honors them, and the dropdown anchors to the wrapper — under the input, as expected.

wrapper CSS: transform: translateZ(0)

3. Ancestor with <code>container-type</code> (the heuristic-override case)

container-type establishes a containing block per spec, and Floating UI's default getOffsetParent walks up to use the wrapper. But for some real-world layouts (notably pure-admin's .pa-layout__main wrapping a shadow-DOM component), the browser does not actually anchor the fixed panel there — and Floating UI's coordinates end up offset by the wrapper's viewport-x, leaving the dropdown stranded to the side of the input.

Since v1.10.1 the library installs a custom getOffsetParent that ignores container-type and contain in favor of properties the browser reliably honors. The dropdown lands under the input even in this configuration.

wrapper CSS: container-type: inline-size (same as .pa-layout__main)

4. The drift-detection warning (interactive)

The library's containing-block heuristic isn't bulletproof — an ancestor can genuinely anchor fixed positioning (per spec) without being on the library's reliable-properties list. Every dropdown position pass verifies the panel landed where the library told the browser to put it. If it drifts, a console.warn fires once per multiselect instance with the likely culprit element and its responsible CSS property.

Open DevTools → Console before toggling the wrapper, then open the dropdown. With contain: paint applied, the dropdown drifts and the warning fires.

wrapper CSS: (plain)
Open the dropdown to position it, then toggle the wrapper CSS.
What the warning looks like
[@keenmate/web-multiselect] Dropdown panel rendered Npx / 0px away from where the
library positioned it. Most likely culprit: <div.form-group#drift-wrapper> (has
contain: paint). An ancestor of <web-multiselect> establishes a fixed-positioning
containing block that the library's heuristic doesn't recognize. Fix on your side:
replace the property with `transform: translateZ(0)` on that ancestor, OR move the
trigger out of that ancestor's subtree. If neither is acceptable, please file an
issue at https://github.com/keenmate/web-multiselect/issues with the ancestor's
computed CSS.

Background

The CSS spec lists seven properties as containing-block-establishing for fixed positioning: transform, perspective, filter, backdrop-filter, qualifying will-change, contain (layout|paint|strict|content), and container-type. In isolation, browsers honor all of them.

But in some shadow-DOM scenarios — a web-multiselect dropdown lives inside the shadow root of a custom element, while the containment-establishing ancestor is in light DOM — the browser's actual position: fixed layout disagrees with Floating UI's prediction for contain and container-type. The library resolves that disagreement in favor of what every browser reliably does, and the drift detector catches whatever falls through.

If you hit the warning in your own app, the two consumer-side fixes are:

  • Replace contain: … or container-type: … on the ancestor with transform: translateZ(0) — same containing-block effect, but the browser definitively anchors fixed positioning to it.
  • Move the <web-multiselect> to a different subtree that doesn't have the problematic ancestor.