Build a Tooltip and Popover System

Updated · techinterview.org

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 — it describes the element it’s attached to and disappears the moment you look away, so it can never hold a link or a button.
  • Popover: richer floating UI on click/tap. May be interactive — it can contain a form, menu, or set of actions, stays open until dismissed, and takes focus like a small dialog.

Different ARIA roles, different UX patterns.

Positioning

The hard part. Position must:

  • Stay near the trigger element — compute the floating element’s coordinates from the trigger’s bounding rect every time it opens. Interviewers want to see you read getBoundingClientRect() rather than hardcode pixel offsets that break at different zoom levels or font sizes.
  • Stay inside the viewport (flip / shift if it would overflow) — a tooltip anchored near the right edge should flip to the left side, or shift inward, instead of spilling off-screen. This is the single most common thing an interviewer tests, usually by putting the trigger in a corner.
  • Stay relative to scroll — when the page or a scroll container moves, the floating element must follow the trigger or hide. Recompute on scroll and resize, and remember nested scroll containers, not just the window.
  • Avoid covering the trigger — add a small gap (the offset) so the floating element never sits on top of the thing it describes, which would also break hover the instant the cursor lands on the tooltip itself.

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) — the primary desktop trigger; pair them so the tooltip shows while the pointer is over the trigger and hides when it leaves.
  • onFocus / onBlur (keyboard) — keyboard and screen-reader users never fire mouse events, so wiring focus is what makes the tooltip usable at all, not optional polish. Expect the interviewer to ask “how does a keyboard user see this?”
  • Delay before showing (200–500ms) — avoid flash on transient hovers
  • Hide immediately on leave

For popovers:

  • onClick / onTap — toggle the open state on the trigger; unlike a tooltip, the popover stays open until the user dismisses it.
  • Click outside to dismiss — listen for a pointer event outside both the popover and its trigger, then close. Watch the edge case where clicking the trigger again shouldn’t close then instantly reopen.
  • Escape to dismiss — close on the Escape key and move focus back to the trigger so the keyboard user isn’t stranded.

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 — mirrors the OS convention for revealing labels, but you have to run the timer yourself and cancel it if the finger starts moving into a scroll.
  • Tap (then dismiss on tap-elsewhere) — this effectively turns the tooltip into a small popover, which is fine when the label is worth a deliberate tap.
  • Native title attribute (accessible, system-styled) — zero code and screen-reader friendly, but you can’t style it and it appears slowly; reserve it for non-essential hints.

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) — links or buttons inside a role="tooltip" can’t be reached by keyboard because focus never moves into it; that content belongs in a popover dialog.
  • No flip/shift; tooltip overflows viewport — test every tooltip with its trigger near a screen edge, or it gets clipped and can force a stray scrollbar.
  • No keyboard support (mouse-only) — hover-only tooltips are invisible to keyboard and screen-reader users, so always add the focus handlers alongside the hover ones.
  • aria-describedby missing — without it the tooltip text is never announced, leaving assistive-tech users with an unlabeled control.
  • Tooltip persists after element scrolls off-screen — hide the floating element once its trigger leaves the viewport; autoUpdate plus a hide middleware handles this.

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.

newsletter

What's actually being asked right now

Interview patterns & comp trends, straight to your inbox.

No spam. Unsubscribe anytime.

newsletter

What's actually being asked right now

Interview patterns & comp trends, straight to your inbox.

No spam. Unsubscribe anytime.

1972 Soviet postage stamp commemorating the Mars 2 probe

worth a read

Mars For The Rest of Us — a weekly-or-more deep dive on the technical side of Mars exploration: rocket propulsion, microbiology, mission architecture, and everything in between. Written by Maciej Ceglowski.

Read it on Substack
Scroll to Top