I'm a core dev and maintainer of Popper and creator of Tippy, two popular libraries used to build tooltips, popovers, dropdowns, menus, etc. in web applications. After working on these things for a few years, I would like to share the knowledge I've accumulated during this time. These elements are absolutely everywhere in web applications, so understanding how they work is important for creating a good user experience!
Disclaimer: This isn't truly everything I know, because there are so many nuances and subtleties that are difficult to get across in a single article 😅. For this article I am focusing on the key problems and solutions to them.
In their very essence, poppers are elements that "pop out" from the normal flow of the document (absolute or fixed position) and float near a reference or target element (such as a button), overlaid on top of the UI. Conceptually, the CSS used to position them is straightforward: you set
position: absolute on the element and some
left coordinates to position it next to the reference element in some placement (top, bottom, left, or right).
However, the calculations performed to receive those coordinates is difficult. While we could naively calculate the coordinates to center it above a button for example, we quickly run into edge cases that a robust solution such as Popper solves for you. For reusable component libraries, this is important, because we don't want the user to manually calculate the coordinates for a popper to make it fit on screen as best as possible every time they add one to their UI. Ideally we just want to call a
position() function that will do this hard work automatically.
As an initial step, you'll place the popper somewhere next to the reference element somewhere in your UI. To do this, you'll choose one of 12 placements: four base ones (
left) or ones with a variation attached (
Problem 1: Preventing overflow if the popper will be clipped or overflow the main axis of a boundary
When the reference element is near the edge of the boundary, for example the left edge of the window, and the tooltip is wider than it, we run into a problem: some of the tooltip text gets cut off and is unreadable. If it's positioned on the left, the text will be clipped and unable to be viewed, while on the right, it will cause overflow and require scrolling to view (LTR layout).
How do we solve this? We calculate how much it's overflowing the boundary and shift it in view to prevent this. In Popper, this is called the
Popper also allows you to specify the whitespace or padding between the popper and the boundary. In this example and by default it lies flush with the boundary edge.
Now we've prevented the left/right overflow — but we have another problem.
When the reference element is near the edge of a boundary, for example the top edge of the window, the tooltip will be cut off. This is similar to the prevent overflow issue in the previous problem, but happening on the alternate axis.
However, this time we can't prevent overflow on this axis (y), because if we do so, the popper will overlap the reference element and obscure its contents. Sometimes this may be acceptable but in most cases it is not (though Popper allows you to do this!).
Instead, we want to flip it to a different placement that fits better entirely rather than just shifting it into view. In Popper, this is called the
Solving these two problems is the core of Popper, but far from all of it.
Your reference element and popper could be located anywhere in the DOM. For example your reference element and popper element could be in different scrolling containers. Popper needs to handle any DOM context you throw at it.
Take the following HTML:
<div id="scroll"> Lots of text here <button id="reference">My Button</button> Lots of text here </div> <div id="tooltip">My Centered Tooltip</div>
When the scrolling container is scrolled, the reference element will change its location on the screen. However your tooltip is not "aware" of this happening, and so it won't track the reference element as it's scrolling.
To solve this, Popper attaches a scroll listener to the container and on each fired scroll event, recalculates the position of the popper. This allows it to stay stuck to the reference element like it should. (This also applies to the main
You may think this would be a performance issue, but calculations showed that even on slow hardware such as low-end mobile phones, this should still be completed within 10ms (frame budget + overhead). Popper is written in an efficient manner to ensure this.
You may also notice that the popper is overflowing the red boundary. In this case, the scrolling container is not a "root" like the viewport, and not a clipping parent of the popper because it's outside of it in the DOM, so we can actually still center it.
Absolutely positioned elements are positioned relative to their
offsetParent (one that's
positioned). This ties in with Problem 3 in terms of DOM context —
offsetParents create an issue:
getBoundingClientRect() is required for getting the reference element's rect, but it's always relative to the viewport. If the popper is not positioned relative to the viewport, we need to consider its
offsetParent when measuring the reference element.
In the above image, we need to subtract the
y coordinates from the popper's
y coordinate offsets, otherwise, we'd end up with the incorrectly-positioned red hachured box below. Reason being, the reference element is closer to the
offsetParent's (0, 0) coordinates than the viewport, so the popper will be positioned too far away.
The reference element and popper element being in different clipping contexts can pose a problem. The popper can appear "detached" from the reference element, or attached to nothing at all if the reference element is fully clipped and hidden from view.
Popper attaches attributes in the following cases:
data-popper-escaped: When the popper escapes the reference element's clipping container (it appears detached)
data-popper-reference-hidden: When the reference element is hidden from view (it appears attached to nothing)
This allows you to fade out or hide the popper once it can no longer appear to be floating near something.
So far, we've talked about the main popper box. But in each of the images shown, there's a little arrow (caret or triangle) that always points to the center of the reference element that is placed outside of the popper box.
While the arrow itself isn't a problem, there are several problems created by it.
- The arrow attempts to stay centered relative to the reference at all times, however, there are cases where this is impossible, such as the following:
We need to constrain the arrow within the popper box at all times. This poses some aesthetic issues, so Popper also enables you to hide the arrow in this case if undesirable.
- If the reference is "point-like", the arrow needs to change shape — interpolate itself using how much it's offset from its ideal position in order to point toward the reference element:
Popper provides this data in the form of
We can't assume we're dealing with a "real" element to position relative to. You should be able to position your popper next to a "virtual" element (one that does not actually exist on the DOM) for usage with
Sometimes even the
flip modifier won't be adequate, because the popper is intrinsically too large in dimensions to fit within the screen. While there are workarounds for this, such as setting
max-width: 100vw for the top/left placements, Popper enables the ability to dynamically resize the popper to fit within the available viewport space when it's at any location on the screen.
Although this is not included by default, it's available as a community package written with a few lines of code, which showcases Popper's powerful extensibility.
Browsers are inconsistent, duh 😔🤚! Here's a non-exhaustive list of issues we found while working on the Popper 2 rewrite:
- Firefox returns
offsetParentfor fixed elements, when it should be
null(per the spec)
- Edge and IE always report
- Safari's elastic overscroll causes
fixedpoppers to translate incorrectly with the overscroll
style.transformupdates are laggy and not 1:1 in-sync. Preventing overflow therefore is not 100% smooth. Other browsers aren't perfect either but much better.
- We use
translate3d()on high PPI devices only for performance and translation smoothness, but on low PPI displays with Windows scaling enabled, it can cause blurring issues.
To conclude, I hope you learned a bit more about why Popper exists and the key problems it solves for these elements in our UIs. I also encourage you to start using the library if not already! Popper solves all of these problems elegantly without you needing to reinvent the wheel. We spent hundreds of hours developing the library and ran into, and fixed, tons of edge cases. Take a look at our visual test snapshots!