Back to Blog
When Your Popovers and Tooltips Need to Be Perfect: Floating UI
7 min readSep 1, 20266 views

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.

FrontendUIWeb DevelopmentTooling
Share

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:

bash
npm install @floating-ui/react

Now, let's build a Tooltip component:

typescript
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:

  1. useFloating Hook: This is the core. We pass it our desired placement ('top') and an array of middleware functions. offset, flip, shift, and arrow are all part of Floating UI's toolkit to refine the positioning.
  2. refs: The useFloating hook provides refs.setReference and refs.setFloating. We attach these to our button (reference) and tooltip div (floating) respectively. This tells Floating UI which elements to observe.
  3. x, y, strategy: These are the calculated CSS properties Floating UI gives us. strategy will typically be 'absolute' or 'fixed', and x, y are the left and top pixel values.
  4. middlewareData.arrow: This object contains the calculated x and y for the arrow element, allowing FloatingArrow to draw itself correctly.
  5. FloatingArrow Component: A helper component provided by @floating-ui/react that uses the middlewareData.arrow to render and position a beautiful SVG arrow that points from the tooltip to the reference. The staticSide prop 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-index is still your responsibility. You need to ensure your floating elements are rendered at a higher z-index than 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, role attributes, 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.

More from the blog
Available for projectsReady to make something fun 🎈

Ready to build the next system?Wanna build something awesome together?

Currently accepting high-impact opportunities in frontend engineering and scalable web applications.Got a cool idea rattling around? Let's grab a virtual coffee and turn it into something people love. ☕