Dismissing context menus, dropdown pickers, and modals when a user clicks outside the element is a staple pattern in UI development.
The Hook Implementation
import { useEffect, type RefObject } from "react";
export function useClickOutside<T extends HTMLElement = HTMLElement>(
ref: RefObject<T | null>,
handler: (event: MouseEvent | TouchEvent) => void
): void {
useEffect(() => {
const listener = (event: MouseEvent | TouchEvent) => {
const el = ref?.current;
// Do nothing if clicking target element or descendent elements
if (!el || el.contains((event.target as Node) || null)) {
return;
}
handler(event);
};
document.addEventListener("mousedown", listener);
document.addEventListener("touchstart", listener);
return () => {
document.removeEventListener("mousedown", listener);
document.removeEventListener("touchstart", listener);
};
}, [ref, handler]);
}Example Usage
import { useState, useRef } from "react";
import { useClickOutside } from "./useClickOutside";
export function DropdownMenu() {
const [isOpen, setIsOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
// Close menu when clicking outside
useClickOutside(menuRef, () => setIsOpen(false));
return (
<div ref={menuRef} className="relative inline-block">
<button onClick={() => setIsOpen((prev) => !prev)}>
Toggle Options
</button>
{isOpen && (
<div className="absolute right-0 mt-2 w-48 bg-white shadow-lg rounded-md p-2">
<p className="p-2 hover:bg-gray-100 cursor-pointer">Profile</p>
<p className="p-2 hover:bg-gray-100 cursor-pointer">Settings</p>
<p className="p-2 hover:bg-gray-100 cursor-pointer text-red-500">Log out</p>
</div>
)}
</div>
);
}