typescriptintermediate

Portal Tooltip Component

Accessible tooltip rendered via Portal with automatic positioning and arrow indicator.

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

interface TooltipProps {
  content: string;
  children: ReactNode;
  position?: 'top' | 'bottom';
}

export function Tooltip({ content, children, position = 'top' }: TooltipProps) {
  const [visible, setVisible] = useState(false);
  const [coords, setCoords] = useState({ top: 0, left: 0 });
  const triggerRef = useRef<HTMLSpanElement>(null);

  useEffect(() => {
    if (!visible || !triggerRef.current) return;
    const rect = triggerRef.current.getBoundingClientRect();
    setCoords({
      top: position === 'top' ? rect.top + window.scrollY - 8 : rect.bottom + window.scrollY + 8,
      left: rect.left + window.scrollX + rect.width / 2,
    });
  }, [visible, position]);

  return (
    <>
      <span
        ref={triggerRef}
        onMouseEnter={() => setVisible(true)}
        onMouseLeave={() => setVisible(false)}
        aria-describedby={visible ? 'tooltip' : undefined}
      >
        {children}
      </span>
      {visible && createPortal(
        <div
          id="tooltip"
          role="tooltip"
          className="fixed px-2 py-1 bg-gray-800 text-white text-xs rounded shadow-lg -translate-x-1/2 whitespace-nowrap z-50"
          style={{ top: coords.top, left: coords.left, transform: `translateX(-50%) translateY(${position === 'top' ? '-100%' : '0'})` }}
        >
          {content}
        </div>,
        document.body
      )}
    </>
  );
}

Use Cases

  • Icon button labels
  • Abbreviated text explanations

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.