useClickOutside
Detect clicks outside a referenced element.
Use Cases
- Close dropdowns when clicking outside
- Dismiss modals on backdrop click
- Close popovers and tooltips
- Deselect items when clicking away
Code
import { useEffect, useRef, RefObject } from 'react';
export function useClickOutside<T extends HTMLElement = HTMLElement>(
handler: () => void,
enabled: boolean = true
): RefObject<T> {
const ref = useRef<T>(null);
useEffect(() => {
if (!enabled) return;
const handleClick = (event: MouseEvent | TouchEvent) => {
const target = event.target as Node;
if (ref.current && !ref.current.contains(target)) {
handler();
}
};
document.addEventListener('mousedown', handleClick);
document.addEventListener('touchstart', handleClick);
return () => {
document.removeEventListener('mousedown', handleClick);
document.removeEventListener('touchstart', handleClick);
};
}, [handler, enabled]);
return ref;
}Usage
Dropdown Menu
import { useState } from 'react';
import { useClickOutside } from '@/hooks/useClickOutside';
function Dropdown() {
const [isOpen, setIsOpen] = useState(false);
const ref = useClickOutside<HTMLDivElement>(() => setIsOpen(false), isOpen);
return (
<div ref={ref} className="relative">
<button onClick={() => setIsOpen(!isOpen)}>
Menu
</button>
{isOpen && (
<div className="absolute top-full mt-2 w-48 rounded-md border bg-white shadow-lg">
<a href="#" className="block px-4 py-2 hover:bg-gray-100">Option 1</a>
<a href="#" className="block px-4 py-2 hover:bg-gray-100">Option 2</a>
<a href="#" className="block px-4 py-2 hover:bg-gray-100">Option 3</a>
</div>
)}
</div>
);
}Modal with Backdrop
function Modal({ isOpen, onClose, children }) {
const ref = useClickOutside<HTMLDivElement>(onClose, isOpen);
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div
ref={ref}
className="w-full max-w-md rounded-lg bg-white p-6 shadow-xl"
>
{children}
</div>
</div>
);
}Search with Suggestions
function SearchWithSuggestions() {
const [query, setQuery] = useState('');
const [showSuggestions, setShowSuggestions] = useState(false);
const ref = useClickOutside<HTMLDivElement>(
() => setShowSuggestions(false),
showSuggestions
);
return (
<div ref={ref} className="relative">
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
onFocus={() => setShowSuggestions(true)}
placeholder="Search..."
/>
{showSuggestions && query && (
<ul className="absolute top-full mt-1 w-full rounded-md border bg-white shadow-lg">
<li className="px-4 py-2 hover:bg-gray-100">Suggestion 1</li>
<li className="px-4 py-2 hover:bg-gray-100">Suggestion 2</li>
</ul>
)}
</div>
);
}Variant: Multiple Refs
Handle multiple elements that should not trigger close:
export function useClickOutsideMultiple<T extends HTMLElement = HTMLElement>(
refs: RefObject<T>[],
handler: () => void,
enabled: boolean = true
): void {
useEffect(() => {
if (!enabled) return;
const handleClick = (event: MouseEvent | TouchEvent) => {
const target = event.target as Node;
const isOutside = refs.every(
(ref) => ref.current && !ref.current.contains(target)
);
if (isOutside) {
handler();
}
};
document.addEventListener('mousedown', handleClick);
document.addEventListener('touchstart', handleClick);
return () => {
document.removeEventListener('mousedown', handleClick);
document.removeEventListener('touchstart', handleClick);
};
}, [refs, handler, enabled]);
}