The wobbling tooltip problem

A Web Developer based in Belgium, specialized in PHP & JS development
A while ago I wrote a small article about modern tooltips.
The idea was simple: use modern browser features such as the Popover API, keep the implementation small, and avoid pulling in a large positioning library for something that looks fairly straightforward.
It worked well.
Except for one tiny thing I only really noticed later on a real mobile device: while scrolling, the tooltip could wobble a few pixels around its reference element.
My old implementation actually has exactly the ingredients for this problem: the tooltip is position: fixed, while scroll events schedule a reposition through requestAnimationFrame.
On desktop this is barely noticeable. On mobile it can be surprisingly visible.
Why Tippy looks so stable
If you compare this with Tippy.js, Tippy often looks rock solid while scrolling.
Tippy uses Popper underneath. Popper's traditional model is based around an absolutely positioned floating element. If both elements are effectively moving with the same document scroll, the browser moves both of them at the same time.
Of course, position: absolute comes with its own price. Absolute positioning is relative to a containing block. Once you start supporting positioned ancestors, scroll containers, transforms, clipping boundaries and portals, finding the correct coordinates gets complicated very quickly.
That is a large part of what Popper does for you.
So Popper works extremely well, but it solves a genuinely complicated problem.
Then came Floating UI
Floating UI is essentially the next generation of Popper. It forked Popper 2, made the API lower-level, smaller and tree-shakeable, and separated one positioning calculation from the code that keeps it updated.
The model is very attractive:
computePosition(reference, floating);
autoUpdate(reference, floating, update);
You decide where the floating element lives and how its styles are applied.
This works very well, but it exposes an important limitation more clearly: when the user scrolls, the browser moves the reference immediately; the floating element does not move with it.
JavaScript receives the scroll, recomputes its position, and moves the floating element afterwards: on mobile, scrolling can happen asynchronously from the main JavaScript thread, and for a short moment, the two elements are therefore at different positions.
That is the wobble.
There is a Floating UI issue dedicated to exactly this problem. Even animationFrame: true does not fundamentally solve it. The maintainer explains that the demos tend not to show the problem because the reference and floating elements move in the same scrolling context.
autoUpdate() is still the right tool for resize, layout shifts, flipping and overflow handling, but JavaScript cannot retroactively move something at the same instant as an asynchronous browser scroll.
A third option
While working on my tiny @lekoala/floating positioning helper, I realized there is a useful middle ground for modern popovers.
An open popover lives in the top layer.
And an absolutely positioned element in the top layer uses the initial containing block, rather than some arbitrary positioned ancestor.
That gives us a very convenient coordinate system.
getBoundingClientRect() already gives us viewport coordinates:
const {x, y} = reference.getBoundingClientRect();
To turn those into document coordinates we only need the document scroll:
left = x + scrollX;
top = y + scrollY;
So floating now supports:
reposition(reference, tooltip, {
placement: "top",
coordinateSpace: "document",
});
with:
.tooltip[popover] {
position: absolute;
inset: auto;
}
The interesting part is what happens afterwards.
When the page scrolls, there is no JavaScript correction required to keep them attached during the page scroll itself.
autoUpdate() is still there. It still recalculates flipping, shifting, available height and nested scroll changes. But it no longer needs to chase the reference simply because the document moved.
Not a universal solution
There is an important limitation.
This only works when the reference itself moves with the document.
A fixed or sticky reference should still use viewport coordinates and position: fixed.
A reference inside an independent scroll container also still needs JavaScript tracking.
So the rule in floating is deliberately small:
normal document reference
→ document coordinates + absolute
fixed / sticky / viewport-anchored reference
→ viewport coordinates + fixed
nested scroller
→ JS still follows
clientX/clientY context menu
→ viewport coordinates
This is not an attempt to replace everything Popper or Floating UI can do.
Quite the opposite: it avoids implementing offset-parent resolution, transformed ancestors and all the other machinery required for fully general absolute positioning.
It just takes advantage of one particularly nice property of modern top-layer popovers.
Sometimes the browser should do the moving
I originally thought the answer to the wobble would be faster scroll listeners, more requestAnimationFrame, or perhaps transforms.
It wasn't.
The better answer was to stop asking JavaScript to do something the browser could already do naturally.
For normal page scrolling, putting the reference and its floating surface in compatible coordinate spaces means they move together automatically.
Sometimes the best positioning update is the one you don't have to run.
Try it in Actual CSS
This is no longer just an experiment in @lekoala/floating.
Starting with Actual CSS v0.8, tooltips use this coordinate-space strategy automatically. A tooltip anchored to normal document content uses document coordinates so it moves naturally with page scrolling, while fixed, sticky, modal, and other viewport-anchored references stay in viewport coordinates.
In other words: the framework now makes the choice for you, and the mobile scroll wobble should be gone in the common case.
Haven't found any issues with it (so far!), so hopefully this is now my new recommended approach.



