useOnClickOutside hook
| 1 min read
Every dropdown, popover, or modal eventually needs the same behaviour — close when you click outside. Here’s a typed React hook that handles it cleanly.
import { useEffect, type RefObject } from "react";
export function useOnClickOutside<T extends HTMLElement>(
ref: RefObject<T>,
handler: (event: MouseEvent | TouchEvent) => void,
) {
useEffect(() => {
const listener = (event: MouseEvent | TouchEvent) => {
const el = ref.current;
if (!el || el.contains(event.target as Node)) return;
handler(event);
};
document.addEventListener("mousedown", listener);
document.addEventListener("touchstart", listener);
return () => {
document.removeEventListener("mousedown", listener);
document.removeEventListener("touchstart", listener);
};
}, [ref, handler]);
}
Usage:
function Dropdown() {
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useOnClickOutside(ref, () => setOpen(false));
return (
<div ref={ref}>
<button onClick={() => setOpen((o) => !o)}>Menu</button>
{open && <ul>{/* items */}</ul>}
</div>
);
}
A few notes worth knowing:
- Listening on
mousedown(notclick) means the handler fires before any focus changes — feels snappier. touchstartcovers mobile.el.contains(event.target)is the key check — anything inside the ref’d element doesn’t count as “outside”.
If your dropdown renders into a portal, you’ll need to also exclude the portal element from “outside” — typically with a second ref or a data attribute check.