跪拜 Guibai
← All articles
Frontend

How Floating UI Measures, Positions, and Keeps Popovers Alive

By Yinlin124 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Every dropdown, tooltip, and popover that escapes a scroll container or clipping parent needs a positioning update strategy. The IntersectionObserver framing trick replaces expensive `getBoundingClientRect` polling with a passive, main-thread-friendly detection mechanism that catches layout shifts most developers don't monitor.

Summary

Floating UI's positioning layer runs a middleware pipeline: `computePosition` calculates the floating element's top-left corner relative to a reference, then `offset`, `flip`, `shift`, and `arrow` middleware adjust for spacing, edge avoidance, and arrow placement. The `detectOverflow` function compares the floating rectangle against the intersection of all clipping ancestors plus the viewport, returning overflow amounts that drive flip and shift decisions. A reference can be any object with a `getBoundingClientRect` method — DOM elements, virtual elements representing a caret position, or mouse coordinates.

The `autoUpdate` system handles four categories of position invalidation. Scroll and resize events are straightforward listeners. Layout shifts — where an element moves without scrolling or resizing — are detected by framing an `IntersectionObserver` with negative `rootMargin` values that shrink the observation box to exactly match the reference's current rectangle; any movement drops the intersection ratio below 1 and triggers recalculation. CSS transform animations slip past all three, so an optional rAF per-frame rectangle comparison catches them at higher cost.

A practical Select implementation attaches the dropdown to `body` to escape parent `overflow: hidden` and `z-index` traps, then relies on `autoUpdate` because the reference scrolls inside a local container while the floating element stays fixed on `body`. A Mention component measures the caret position by mirroring input text into a hidden span with identical font properties, producing a virtual element that `computePosition` treats like any other reference.

Takeaways
`computePosition` returns the floating element's top-left coordinates; middleware runs sequentially and can modify x, y, placement, and middlewareData.
`detectOverflow` computes the intersection of all clipping ancestors (overflow: hidden/auto/scroll/clip) plus the viewport, then checks four-direction overflow against the floating rectangle.
A reference passed to `computePosition` only needs a `getBoundingClientRect` method — virtual elements work for cursors, selections, and pointer positions.
Select dropdowns typically attach to `body` to avoid parent overflow clipping and z-index conflicts, which breaks the natural co-scroll relationship and requires `autoUpdate`.
`autoUpdate` uses negative `rootMargin` on an `IntersectionObserver` to shrink the observation box to exactly the reference's current rect; any movement drops the intersection ratio and triggers an update.
CSS transform animations do not trigger scroll, resize, or IntersectionObserver callbacks, so an optional rAF per-frame rectangle comparison catches them.
Measuring a caret position for Mention requires a hidden span with matching font, letterSpacing, and whiteSpace: pre, minus scrollLeft to account for horizontal scroll.
Using `mousedown` instead of `click` for outside-click detection closes the panel before blur and focus events fire.
Conclusions

The negative rootMargin IntersectionObserver trick is a general-purpose technique for detecting element movement without polling getBoundingClientRect — applicable anywhere a DOM node's viewport-relative position needs monitoring.

Floating UI's design separates the measurement strategy from the positioning algorithm: the core works on any platform that can supply rectangles, while autoUpdate is DOM-specific and optimizes for browser event sources.

The four autoUpdate paths form a hierarchy of cost: scroll/resize listeners are cheapest, IntersectionObserver is mid-cost and passive, and rAF polling is most expensive and opt-in — a deliberate tradeoff that avoids burning frames on static UIs.

Attaching popovers to body solves clipping but creates a coordination problem that most hand-rolled implementations ignore until QA finds the dropdown drifting on scroll.

Concepts & terms
detectOverflow
A Floating UI function that compares the floating element's rectangle against the intersection of all clipping ancestor boundaries plus the viewport, returning per-direction overflow amounts (positive means out of bounds).
virtual element
Any plain object with a getBoundingClientRect method that returns a DOMRect-like shape; used as a reference in computePosition when the anchor point is not a real DOM node (e.g., a caret position or mouse coordinate).
layout shift detection via IntersectionObserver
A technique that sets negative rootMargin values on an IntersectionObserver to shrink the observation area to exactly match a reference element's current bounding rect; any movement that changes the element's viewport position drops the intersection ratio below 1, signaling a shift without polling.
middleware pipeline
Floating UI runs middleware functions in array order during computePosition; each can modify the computed x, y, and placement, and write data into middlewareData for later use (e.g., arrow coordinates).
Source: juejin.cn ↗ Google Translate ↗ Backup ↗