React / Types of React Portal / Dropdown / Context Menu Portal
Home /React /Types of React Portal /Dropdown / Context Menu Portal

Dropdown / Context Menu Portal

Dropdowns in tables, cards, or sidebar panels are often clipped. A Portal renders them into
the body and positions them precisely below the trigger button using the same
getBoundingClientRect() technique.

Dropdown.jsx


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

function Dropdown({ label, options, onSelect }) {
  const [open, setOpen] = useState(false);
  const [position, setPosition] = useState({ top: 0, left: 0 });
  const buttonRef = useRef(null);
  const dropdownRef = useRef(null);

  function toggle() {
    if (!open) {
      const rect = buttonRef.current.getBoundingClientRect();
      setPosition({
        top: rect.bottom + window.scrollY,
        left: rect.left + window.scrollX
      });
    }
    setOpen(prev => !prev);
  }

  // Close when clicking outside
  useEffect(() => {
    function handleClick(e) {
      if (
        dropdownRef.current &&
        !dropdownRef.current.contains(e.target) &&
        !buttonRef.current.contains(e.target)
      ) {
        setOpen(false);
      }
    }
    if (open) {
      document.addEventListener('mousedown', handleClick);
    }
    return () => document.removeEventListener('mousedown', handleClick);
  }, [open]);

  return (
    <>
      <button ref={buttonRef} onClick={toggle} aria-haspopup="listbox" aria-expanded={open}>
        {label}
      </button>

      {open && createPortal(
        <ul
          ref={dropdownRef}
          role="listbox"
          style={{
            position: 'absolute',
            top: position.top,
            left: position.left,
            background: '#fff',
            border: '1px solid #ccc',
            listStyle: 'none',
            margin: 0,
            padding: 0,
            zIndex: 9999,
            minWidth: '160px'
          }}
        >
          {options.map((opt, i) => (
            <li
              key={i}
              role="option"
              style={{ padding: '8px 12px', cursor: 'pointer' }}
              onClick={() => { onSelect(opt); setOpen(false); }}
            >
              {opt}
            </li>
          ))}
        </ul>,
        document.body
      )}
    </>
  );
}

export default Dropdown;

Usage


<Dropdown
  label="Select Option"
  options={['Edit', 'Duplicate', 'Delete']}
  onSelect={(opt) => console.log('Selected:', opt)}
/>