Every dropdown, popover, or modal eventually needs the same behaviour — close when you click outside. Here’s a typed React hook that handles it cleanly.

import { useEffect, type RefObject } from "react";

export function useOnClickOutside<T extends HTMLElement>(
  ref: RefObject<T>,
  handler: (event: MouseEvent | TouchEvent) => void,
) {
  useEffect(() => {
    const listener = (event: MouseEvent | TouchEvent) => {
      const el = ref.current;
      if (!el || el.contains(event.target as Node)) return;
      handler(event);
    };

    document.addEventListener("mousedown", listener);
    document.addEventListener("touchstart", listener);

    return () => {
      document.removeEventListener("mousedown", listener);
      document.removeEventListener("touchstart", listener);
    };
  }, [ref, handler]);
}

Usage:

function Dropdown() {
  const [open, setOpen] = useState(false);
  const ref = useRef<HTMLDivElement>(null);

  useOnClickOutside(ref, () => setOpen(false));

  return (
    <div ref={ref}>
      <button onClick={() => setOpen((o) => !o)}>Menu</button>
      {open && <ul>{/* items */}</ul>}
    </div>
  );
}

A few notes worth knowing:

  • Listening on mousedown (not click) means the handler fires before any focus changes — feels snappier.
  • touchstart covers mobile.
  • el.contains(event.target) is the key check — anything inside the ref’d element doesn’t count as “outside”.

If your dropdown renders into a portal, you’ll need to also exclude the portal element from “outside” — typically with a second ref or a data attribute check.