useClickOutside

UI/Interaction

A React hook that detects clicks outside of a specified element, perfect for modals, dropdowns, and popups.

Demo

Click the button to open the dropdown, then click outside to close it:

Installation

npm install @thibault.sh/hooks

Usage

import { useClickOutside } from '@thibault.sh/hooks/useClickOutside';
import { useRef, useState } from 'react';

function Modal() {
  const [isOpen, setIsOpen] = useState(false);
  const modalRef = useRef<HTMLDivElement>(null);

  useClickOutside(modalRef, (event) => {
    if (isOpen && modalRef.current && !modalRef.current.contains(event.target as Node)) {
      setIsOpen(false);
    }
  });

  return (
    <>
      <button onClick={() => setIsOpen(true)}>Open Modal</button>
      {isOpen && (
        <div className="modal-overlay">
          <div ref={modalRef} className="modal-content">
            <h2>Modal Content</h2>
            <p>Click outside to close</p>
          </div>
        </div>
      )}
    </>
  );
}

API

Parameters

  • ref
    RefObject<HTMLElement>

    React ref object for the element to monitor

  • handler
    (event: MouseEvent | TouchEvent) => void

    Callback function to execute when click outside occurs

Type Safety Notes:

  • • The hook accepts refs to any HTML element type (div, button, etc.)
  • • You should check if the ref is available before accessing it in the handler
  • • The event target should be cast to Node when using contains()

Features

  • Touch Support

    Works with both mouse clicks and touch events

  • Type Safety

    Full TypeScript support for element refs

  • Cleanup

    Automatically removes event listeners when component unmounts

  • Performance

    Uses event delegation for efficient handling

Context Menu Example

import { useClickOutside } from '@thibault.sh/hooks';
import { useRef, useState } from 'react';

interface Position {
  x: number;
  y: number;
}

function ContextMenu() {
  const [isOpen, setIsOpen] = useState(false);
  const [position, setPosition] = useState<Position>({ x: 0, y: 0 });
  const menuRef = useRef<HTMLDivElement>(null);

  useClickOutside(menuRef, (event) => {
    if (isOpen && menuRef.current && !menuRef.current.contains(event.target as Node)) {
      setIsOpen(false);
    }
  });

  const handleContextMenu = (e: React.MouseEvent) => {
    e.preventDefault();
    setIsOpen(true);
    setPosition({ x: e.clientX, y: e.clientY });
  };

  return (
    <div onContextMenu={handleContextMenu} className="min-h-screen">
      <p>Right click anywhere to open the context menu</p>
      {isOpen && (
        <div
          ref={menuRef}
          style={{
            position: 'fixed',
            top: position.y,
            left: position.x
          }}
          className="bg-white border shadow-lg rounded-md p-2"
        >
          <button className="block w-full p-2 hover:bg-gray-100">
            Copy
          </button>
          <button className="block w-full p-2 hover:bg-gray-100">
            Paste
          </button>
          <button className="block w-full p-2 hover:bg-gray-100">
            Delete
          </button>
        </div>
      )}
    </div>
  );
}

Tooltip Example

import { useClickOutside } from '@thibault.sh/hooks';
import { useRef, useState } from 'react';

function TooltipWithClickOutside() {
  const [isOpen, setIsOpen] = useState(false);
  const tooltipRef = useRef<HTMLDivElement>(null);

  useClickOutside(tooltipRef, (event) => {
    if (isOpen && tooltipRef.current && !tooltipRef.current.contains(event.target as Node)) {
      setIsOpen(false);
    }
  });

  return (
    <div className="relative inline-block">
      <button
        onClick={() => setIsOpen(!isOpen)}
        className="info-button"
      >
        ?
      </button>
      {isOpen && (
        <div
          ref={tooltipRef}
          className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 p-4 bg-black text-white rounded shadow-lg"
        >
          <h3 className="font-bold mb-2">Help Information</h3>
          <p>This tooltip stays open until you click outside.</p>
          <p>Click anywhere outside to close it.</p>
        </div>
      )}
    </div>
  );
}