React / Types of React Portal / Tooltip Portal
Home /React /Types of React Portal /Tooltip Portal

Tooltip Portal

Tooltip Portal

Tooltips must appear next to the trigger element but above all other content. They break out
of any parent with overflow: hidden using a Portal mounted to document.body.

Tooltip.jsx

import { createPortal } from 'react-dom';
import { useState, useRef, useEffect } from 'react';

function Tooltip({ text, children }) {
  const [visible, setVisible] = useState(false);
  const [position, setPosition] = useState({ top: 0, left: 0 });
  const triggerRef = useRef(null);

  function show() {
    const rect = triggerRef.current.getBoundingClientRect();
    setPosition({
      top: rect.top + window.scrollY - 40,   // 40px above the element
      left: rect.left + window.scrollX + rect.width / 2
    });
    setVisible(true);
  }

  function hide() {
    setVisible(false);
  }

  const tooltip = visible
    ? createPortal(
        <div
          role="tooltip"
          style={{
            position: 'absolute',
            top: position.top,
            left: position.left,
            transform: 'translateX(-50%)',
            background: '#333',
            color: '#fff',
            padding: '4px 10px',
            borderRadius: '4px',
            fontSize: '0.85rem',
            pointerEvents: 'none',
            zIndex: 99999,
            whiteSpace: 'nowrap'
          }}
        >
          {text}
        </div>,
        document.body
      )
    : null;

  return (
    <>
      <span
        ref={triggerRef}
        onMouseEnter={show}
        onMouseLeave={hide}
        onFocus={show}
        onBlur={hide}
        tabIndex={0}
        aria-describedby="tooltip"
      >
        {children}
      </span>
      {tooltip}
    </>
  );
}

export default Tooltip;

Usage

<Tooltip text="This is a helpful tooltip">
  <button>Hover over me</button>
</Tooltip>

Why getBoundingClientRect() + scrollY?

getBoundingClientRect() returns position relative to the viewport.
Adding window.scrollY and window.scrollX converts it to
document-absolute coordinates, which is needed because the tooltip uses
position: absolute relative to the document root.