
When Your Popovers and Tooltips Need to Be Perfect: Floating UI
Building UI components like tooltips, popovers, and dropdowns seems trivial until you account for screen edges, scrolling, and dynamic content. Floating UI is the library that solves these headaches by precisely positioning your floating elements, making them behave perfectly in any scenario.
by Sunil Band
The Perennial Pain of Floating UI
Every frontend developer has been there: you need a tooltip, a popover, or a dropdown menu. You throw a position: absolute on it, maybe a left: 0, top: 100%, and call it a day. Then the QA team finds all the edge cases. The tooltip goes off-screen. It's cut off by an overflow container. It flips upside down unexpectedly. It covers the element it's supposed to be describing. Suddenly, that 'simple' component is a rabbit hole of getBoundingClientRect, scroll listeners, and resize observers.
This isn't just about making things look right; it's about user experience. A poorly positioned popover breaks the flow, forcing the user to re-evaluate their mental model of the UI. It's a subtle but constant friction. We spend so much time making our primary content perfect, but often neglect these transient, yet critical, UI elements.
This is precisely the problem Floating UI (formerly Popper.js) solves. It's not just a utility; it's a dedicated engine for positioning floating elements relative to a reference element, ensuring they stay in view and behave intelligently regardless of their context.
How Floating UI Makes It Easy
Floating UI's core concept is simple: you have a reference element (like a button) and a floating element (like a tooltip). Floating UI then calculates the optimal position for the floating element based on various strategies, modifiers, and available space. It's framework-agnostic, meaning you can use it with React, Vue, vanilla JS, or whatever you prefer.
The library handles a ton of complexity under the hood:
- Placement: It automatically picks the best side (top, bottom, left, right) and alignment (start, center, end) to keep your floating element visible.
- Flipping: If the preferred placement is obstructed, it will flip the floating element to the opposite side to stay on screen.
- Shifting: If flipping isn't enough, it can shift the floating element along the X or Y axis to prevent it from overflowing the viewport.
- Offsetting: You can define a precise distance between the reference and floating elements.
- Arrows: It can position an arrow element to point from the floating element to the reference.
- Scrolling: It re-positions elements when their scroll parents change.
This isn't just about static positioning. Floating UI listens to DOM changes, scrolls, and resizes, and dynamically updates the position of your floating element to ensure it always looks correct.
Let's look at a basic example of how you'd use it in React, which is where I find myself reaching for it most often.
A Realistic React Example
Imagine we want a simple tooltip that appears when hovering over a button. We want it to try to appear top but flip to bottom if there isn't enough space. It also needs a small offset and an arrow pointing to the button.
First, install it:
npm install @floating-ui/reactNow, let's build a Tooltip component:
import React, { useState, useRef, useLayoutEffect } from 'react';
import { useFloating, offset, flip, shift, arrow, FloatingArrow } from '@floating-ui/react';
interface TooltipProps {
content: React.ReactNode;
children: React.ReactElement;
}
export const Tooltip: React.FC<TooltipProps> = ({ content, children }) => {
const [isOpen, setIsOpen] = useState(false);
const arrowRef = useRef<SVGSVGElement>(null);
// useFloating hook orchestrates all the positioning logic
const { x, y, strategy, refs, context, placement, middlewareData } = useFloating({
open: isOpen,
onOpenChange: setIsOpen,
placement: 'top', // Preferred placement
middleware: [
offset(8), // Keep 8px distance from the reference element
flip(), // Flip to the opposite side if 'top' is obstructed
shift({ padding: 5 }), // Shift along the axis to prevent viewport overflow, with 5px padding
arrow({ element: arrowRef }), // Enable arrow positioning
],
});
// Clone the child element to attach the reference ref
const referenceElement = React.cloneElement(children, {
ref: refs.setReference,
onMouseEnter: () => setIsOpen(true),
onMouseLeave: () => setIsOpen(false),
onFocus: () => setIsOpen(true),
onBlur: () => setIsOpen(false),
});
const staticSide = {
top: 'bottom',
right: 'left',
bottom: 'top',
left: 'right',
}[placement.split('-')[0]] as 'top' | 'right' | 'bottom' | 'left';
return (
<>
{referenceElement}
{isOpen && (
<div
ref={refs.setFloating} // Attach the floating ref to the tooltip div
style={{
position: strategy,
top: y ?? 0,
left: x ?? 0,
zIndex: 1000, // Ensure tooltip is above other content
background: 'black',
color: 'white',
padding: '8px 12px',
borderRadius: '4px',
fontSize: '0.9em',
boxShadow: '0 2px 8px rgba(0,0,0,0.2)',
}}
>
{content}
<FloatingArrow
ref={arrowRef} // Attach the arrow ref
context={context}
fill="black" // Match background color
height={8}
width={16}
tipRadius={2}
staticSide={staticSide}
style={{ opacity: middlewareData.arrow?.x == null ? 0 : 1 }} // Hide arrow if position not calculated
/>
</div>
)}
</>
);
};
// Usage example:
function App() {
return (
<div style={{ padding: '200px', display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh', flexDirection: 'column' }}>
<Tooltip content="Hello, I am a tooltip!">
<button style={{ padding: '10px 20px', fontSize: '1.2em' }}>Hover Me</button>
</Tooltip>
<div style={{ marginTop: '100px' }}>
<Tooltip content="Another tooltip, perhaps a bit longer.">
<button style={{ padding: '10px 20px', fontSize: '1.2em' }}>Hover Me Too</button>
</Tooltip>
</div>
</div>
);
}
export default App;Let's break down what's happening:
-
useFloatingHook: This is the core. We pass it our desiredplacement('top') and an array ofmiddlewarefunctions.offset,flip,shift, andarroware all part of Floating UI's toolkit to refine the positioning. -
refs: TheuseFloatinghook providesrefs.setReferenceandrefs.setFloating. We attach these to our button (reference) and tooltip div (floating) respectively. This tells Floating UI which elements to observe. -
x,y,strategy: These are the calculated CSS properties Floating UI gives us.strategywill typically be'absolute'or'fixed', andx,yare theleftandtoppixel values. -
middlewareData.arrow: This object contains the calculatedxandyfor the arrow element, allowingFloatingArrowto draw itself correctly. -
FloatingArrowComponent: A helper component provided by@floating-ui/reactthat uses themiddlewareData.arrowto render and position a beautiful SVG arrow that points from the tooltip to the reference. ThestaticSideprop ensures the arrow aligns correctly based on the final placement.
Now, if you run this, you'll see the tooltip appear above the button. If you resize the browser or scroll the div so that there's no space above the button, the tooltip will automatically flip to the bottom. This automatic, intelligent positioning is what makes Floating UI so powerful.
The Power of Modifiers
The middleware array is where you define the sophisticated behavior. Here are a few other common ones:
-
hide(): Detects if the floating element is completely outside the boundary and provides data to hide it. Useful for tooltips that should disappear if their reference is scrolled off-screen. -
size(): Allows you to adjust the floating element's size (width/height) based on available space. Great for dropdowns that shouldn't overflow the viewport.
These modifiers are composable. You can arrange them in the middleware array to create very specific positioning logic. The order often matters, as one modifier's output can be an input to the next.
Trade-offs and Considerations
While Floating UI is a fantastic tool, it's not a magic bullet that removes all UI complexity. Here are a few things to keep in mind:
- Initial Setup: For the absolute simplest, static tooltip, it might feel like a bit of boilerplate. However, the moment you need any dynamic behavior (flipping, shifting, scroll handling), it quickly pays for itself.
- Performance with Many Elements: If you have hundreds of floating elements constantly updating, you might need to optimize. Floating UI is highly performant, but any DOM manipulation comes with a cost. Consider techniques like virtualizing lists or lazy-rendering less critical elements.
- Z-index Management: Floating UI handles positioning, but
z-indexis still your responsibility. You need to ensure your floating elements are rendered at a higherz-indexthan surrounding content to appear on top. - Accessibility: While Floating UI takes care of the visual positioning, you're still responsible for the accessibility of your floating components. This means adding
aria-labelledby,roleattributes, handling keyboard navigation, and managing focus. Floating UI focuses solely on geometry, not semantics or interaction.
I find the initial boilerplate is a small price to pay for the robustness and peace of mind it offers. The alternative is usually a custom solution that inevitably misses an edge case and requires constant maintenance.
Wrapping up
Building robust, accessible, and visually consistent floating UI components is a harder problem than it looks. Floating UI takes that burden off your shoulders by providing a highly optimized and flexible engine for positioning. It's a fundamental utility that I reach for in almost every project that involves custom dropdowns, tooltips, or popovers.
If you're still wrestling with getBoundingClientRect and useEffect for your floating elements, take an hour to refactor one of your components using Floating UI. Start with a simple tooltip and progressively add modifiers like flip and shift. You'll quickly see how it simplifies complex layout challenges and makes your UI feel much more polished. Clone the example above and try breaking it yourself – then see how Floating UI automatically fixes it.

When Your Frontend Needs to Process Files, Not Just Upload Them: Diving into WebAssembly
Sending user-generated files to a backend for processing often feels like the default, but it's not always the best solution. We'll explore how WebAssembly lets us handle complex file operations directly in the browser, boosting privacy and performance.

When Your Next.js App Needs to Speak Every Language: Diving into next-intl
Internationalization often feels like an afterthought until your product manager drops the 'global market' bomb. Suddenly, every string, date, and currency needs to adapt. While Next.js provides some primitives, a robust i18n solution is crucial, and next-intl makes this surprisingly elegant.

When Your React App Needs a 3D Dimension: Embracing React Three Fiber
Integrating 3D experiences into web applications used to be a mess of imperative Three.js code. React Three Fiber changes that, letting you build complex 3D scenes with familiar React components and state management. It's a game-changer for interactive visualizations and immersive UIs.


















