CSS

Replacing our tooltip and popover positioning library with CSS anchor positioning

CSS

Positioning a tooltip or popover relative to its trigger element used to mean reaching for a dedicated JavaScript library to handle collision detection and viewport edges, tracking scroll position, resize events, and every edge case involved in keeping a floating element visually attached to something that might itself be moving. CSS anchor positioning reached usable browser support this year, and we finally tested removing that dependency entirely across a handful of projects.

Native anchor positioning handled the vast majority of our tooltip and popover cases without a single line of positioning JavaScript, and the small number of edge cases it didn't cover were rare enough to handle with a lightweight fallback rather than keeping the full library around just for them. The core mechanism, declaring an anchor reference in CSS and letting the browser handle the positioning math natively, covers the same collision-avoidance and viewport-awareness behavior the JavaScript library used to compute manually, but the browser does it as part of layout rather than as a separate calculation running in response to scroll and resize events after the fact.

The performance difference was more noticeable than we expected going in. The old library recalculated positioning on scroll and resize using JavaScript event listeners, which worked fine in isolation but added up on pages with many tooltips or popovers active at once, each one running its own positioning calculation independently. Native anchor positioning is handled by the browser's layout engine directly, with no equivalent per-element JavaScript overhead, and dropping the library measurably reduced jank on pages that had previously shown it under heavy tooltip usage.

The edge cases that still needed a fallback were mostly around more exotic positioning scenarios, a popover that needed to reposition based on complex, non-standard trigger geometry that the current anchor positioning specification doesn't yet have a clean declarative way to express. We kept a small amount of positioning JavaScript around specifically for those cases rather than trying to force everything into the native mechanism, since a forced fit there would have cost more engineering time than it saved.

Browser support was the real gating factor for when we felt comfortable making this change, and we didn't move until support was solid enough across our clients' actual user bases to make removing the JavaScript fallback for the common case a safe default rather than a risk. For a project with an unusually old-browser-heavy user base, this migration would need a longer runway or a more careful fallback strategy than we needed here.

Removing a dependency we'd carried for years felt disproportionately satisfying given how contained the change was. Fewer dependencies means fewer version upgrades to track and fewer places a library's own bugs or breaking changes can surface unexpectedly, and this was a case where the native platform genuinely caught up to what the library used to be necessary for.

← Back to the journal

Have a project in mind?
Let’s talk.

Tell us where you are and where you want to go. We'll map the fastest route between the two.

Currently accepting new clients