react

useClickOutside Hook

Detect clicks outside an element - perfect for dropdowns, modals, and popover menus.

#hooks #events #ui

A hook to detect when users click outside a specified element.

useClickOutside Hook

import { useEffect, useRef, RefObject } from 'react';

function useClickOutside<T extends HTMLElement>(
  handler: () => void
): RefObject<T> {
  const ref = useRef<T>(null);

  useEffect(() => {
    const listener = (event: MouseEvent | TouchEvent) => {
      const el = ref.current;
      if (!el || el.contains(event.target as Node)) {
        return;
      }
      handler();
    };

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

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

  return ref;
}

export default useClickOutside;

Usage - Dropdown Menu

function Dropdown() {
  const [isOpen, setIsOpen] = useState(false);
  const dropdownRef = useClickOutside<HTMLDivElement>(() => setIsOpen(false));

  return (
    <div ref={dropdownRef} className="relative">
      <button onClick={() => setIsOpen(!isOpen)}>
        Menu ▾
      </button>
      
      {isOpen && (
        <div className="absolute top-full mt-2 bg-white shadow-lg rounded-lg">
          <a href="/profile">Profile</a>
          <a href="/settings">Settings</a>
          <a href="/logout">Logout</a>
        </div>
      )}
    </div>
  );
}

Usage - Modal

function Modal({ isOpen, onClose, children }) {
  const modalRef = useClickOutside<HTMLDivElement>(onClose);

  if (!isOpen) return null;

  return (
    <div className="fixed inset-0 bg-black/50 flex items-center justify-center">
      <div ref={modalRef} className="bg-white p-6 rounded-xl max-w-lg">
        {children}
      </div>
    </div>
  );
}

With Multiple Refs (Escape Key Support)

function useClickOutside<T extends HTMLElement>(
  handler: () => void,
  enableEscape: boolean = true
): RefObject<T> {
  const ref = useRef<T>(null);

  useEffect(() => {
    const clickListener = (e: MouseEvent | TouchEvent) => {
      if (!ref.current?.contains(e.target as Node)) handler();
    };

    const keyListener = (e: KeyboardEvent) => {
      if (enableEscape && e.key === 'Escape') handler();
    };

    document.addEventListener('mousedown', clickListener);
    document.addEventListener('touchstart', clickListener);
    document.addEventListener('keydown', keyListener);

    return () => {
      document.removeEventListener('mousedown', clickListener);
      document.removeEventListener('touchstart', clickListener);
      document.removeEventListener('keydown', keyListener);
    };
  }, [handler, enableEscape]);

  return ref;
}

Common uses:

  • Dropdown menus
  • Modal dialogs
  • Popover tooltips
  • Search autocomplete
  • Date pickers