Build a Tooltip and Popover System

Tooltips and popovers seem trivial until you implement them. The interview probes whether you understand viewport-aware positioning, accessibility (which differ for tooltips vs popovers), gesture handling, and the production patterns that make these elements feel polished.

Tooltip vs popover

  • Tooltip: small text label on hover/focus. Non-interactive.
  • Popover: richer floating UI on click/tap. May be interactive.

Different ARIA roles, different UX patterns.

Positioning

The hard part. Position must:

  • Stay near the trigger element
  • Stay inside the viewport (flip / shift if it would overflow)
  • Stay relative to scroll
  • Avoid covering the trigger

Library: Floating UI (formerly Popper.js) is the industry standard. Handles all positioning math.

Floating UI primitives

  • autoUpdate: reposition on scroll, resize, content change
  • offset: pixels from trigger
  • flip: flip to other side if no room
  • shift: shift along axis to stay in viewport
  • arrow: position the arrow pointer

Tooltip implementation

Standard React pattern with Floating UI:

const { refs, floatingStyles } = useFloating({
  middleware: [offset(8), flip(), shift()],
  whileElementsMounted: autoUpdate
});

<button ref={refs.setReference} {...handlers}>Hover me</button>
{open && (
  <div ref={refs.setFloating} style={floatingStyles}>Tooltip</div>
)}

Trigger events

For tooltips:

  • onMouseEnter / onMouseLeave (hover)
  • onFocus / onBlur (keyboard)
  • Delay before showing (200–500ms) — avoid flash on transient hovers
  • Hide immediately on leave

For popovers:

  • onClick / onTap
  • Click outside to dismiss
  • Escape to dismiss

Hover delay

Without delay, tooltip flashes annoyingly when user moves cursor through. Standard:

  • 200ms before show on hover
  • 0ms before hide on leave

Cancel show timer if user leaves before delay completes.

Accessibility

Tooltip

  • aria-describedby on the trigger pointing to the tooltip ID
  • Tooltip has role="tooltip"
  • Trigger is naturally focusable (button) so keyboard users see it
  • Don’t put interactive content in a tooltip

Popover

  • Trigger has aria-haspopup="dialog" and aria-expanded
  • Popover has role="dialog"
  • Focus moves into popover on open
  • Focus returns on close
  • Trap focus inside popover (especially for forms)

Touch devices

Tooltips on touch are awkward — there is no hover. Patterns:

  • Long-press to show
  • Tap (then dismiss tap-elsewhere)
  • Native title attribute (accessible, system-styled)

The HTML popover API

2024 native API:

<button popovertarget="my-popover">Open</button>
<div id="my-popover" popover>
  Content
</div>

Browser handles light dismiss, focus trap, top layer rendering. Modern browsers support; older fallback to library.

Common mistakes

  • Tooltip with interactive content (use popover)
  • No flip/shift; tooltip overflows viewport
  • No keyboard support (mouse-only)
  • aria-describedby missing
  • Tooltip persists after element scrolls off-screen

Frequently Asked Questions

Floating UI or Tippy.js?

Floating UI is the modern primitive. Tippy.js was built on Popper.js (now Floating UI). Use Floating UI for new projects.

Should I use the native HTML popover API?

For modern apps targeting recent browsers: yes. Some edge cases need library fallback.

How do I handle a tooltip on a disabled button?

Disabled buttons don’t fire mouse events. Wrap in a span with the trigger handlers, or use pointer-events: none on the button itself.

Scroll to Top