You're building a modal, dropdown, or menu. You want it to close when users click outside of it. Here's the clean way to do it.
The concept
Use Node.contains() to check if the clicked element is inside your target element. If it's not — the user clicked outside.
Basic implementation
import { useEffect, useRef } from 'react';
function Modal({ isOpen, onClose, children }) {
const modalRef = useRef<HTMLDivElement>(null);
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (modalRef.current && !modalRef.current.contains(event.target as Node)) {
onClose();
}
}
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
<div ref={modalRef} className="modal">
{children}
</div>
);
}
How it works
modalRef.currentpoints to your modal elementevent.targetis what the user clickedcontains()returnstrueif the clicked element is inside the modal- If it returns
false— click was outside, so close the modal
Handle touch devices
Add touchstart for mobile:
useEffect(() => {
function handleClickOutside(event: MouseEvent | TouchEvent) {
if (modalRef.current && !modalRef.current.contains(event.target as Node)) {
onClose();
}
}
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('touchstart', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('touchstart', handleClickOutside);
};
}, [isOpen, onClose]);
Don't close when clicking inside
The !modalRef.current.contains(event.target) check already handles this — clicks inside the modal are ignored.
That's it. One ref, one useEffect, and your modal closes when users click anywhere outside.