useClickOutside Hook for React
A typed React hook that fires a callback when the user clicks outside a referenced element. Perfect for closing dropdowns and modals.
useClickOutside is a React hook that calls a callback whenever the user clicks anywhere outside a referenced element. This is the standard pattern for closing dropdowns, modals, popovers, and tooltips when the user clicks away. Listens on mousedown so it fires before any inner click handler.
Tested on React 19, modern browsers.
When to Use This
- Closing a dropdown menu when the user clicks outside it
- Dismissing a modal or sheet on backdrop click
- Hiding a tooltip when focus moves elsewhere
- Auto-collapsing an expanded card
Don't use this when the element is rendered through a portal that the ref doesn't cover (use a custom contains check) or when you need to detect clicks on a specific element only (use a regular click handler).
Code
import { useEffect, type RefObject } from 'react';
export function useClickOutside<T extends HTMLElement>(
ref: RefObject<T | null>,
handler: (event: MouseEvent | TouchEvent) => void
): void {
useEffect(() => {
function 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]);
}The hook is generic over the element type, so you get full TypeScript inference when you pass a ref. mousedown and touchstart cover both desktop and mobile interaction. el.contains is the DOM API for "is this node inside this element?".
Usage
A dropdown menu that closes when the user clicks outside it:
'use client';
import { useRef, useState } from 'react';
import { useClickOutside } from '@/hooks/useClickOutside';
export function UserMenu() {
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useClickOutside(ref, () => setOpen(false));
return (
<div ref={ref} className="relative">
<button onClick={() => setOpen((prev) => !prev)}>
Menu
</button>
{open && (
<div className="absolute top-full mt-2 bg-white shadow-lg rounded p-2">
<button>Profile</button>
<button>Settings</button>
<button>Sign out</button>
</div>
)}
</div>
);
}The wrapping div is the boundary. Clicks inside it (including the trigger button and menu items) keep the menu open. Clicks anywhere else close it.
Things to Know
- The
handlershould be stable. Pass it viauseCallbackor define it inline only if you don't mind the effect re-running on every render. The latter is fine for simplesetOpen(false)patterns but wasteful for heavy callbacks. mousedownversusclickmatters. Withclick, child button handlers run first, then the outside handler closes the dropdown. Withmousedown, the dropdown closes before the click event ever fires. Pickmousedownfor "click-away" semantics,clickfor "click-on-the-action" semantics.- Portals break the contains check. If your menu renders into
document.bodyviacreatePortal, the portal's content is not a descendant of your ref. You need a second ref that covers the portal, or useevent.composedPath()for shadow DOM compatibility. - Touchstart fires before mousedown on mobile. Listening on both is fine — the handler is idempotent if you only call
setOpen(false). - Don't use this for ESC key dismissal. That's a separate concern. Use
useKeyboardShortcutfor it.
Related Snippets & Reading
- useKeyboardShortcut Hook(coming soon) — pairs with click-outside for ESC dismissal
- useDebounce Hook for React — debounce search inputs inside the dropdown
- MDN Node.contains docs — the DOM check this hook uses
Frequently Asked Questions
Why listen on mousedown instead of click?
mousedown fires before any click handler runs inside the element, so you can decide whether to close the dropdown before any nested button has a chance to act. Listening on click means a child button click would close the dropdown after the button's handler runs, which often causes layout flicker.
Does useClickOutside work with portal-rendered elements?
Yes, but only if the portal content is also wrapped in a ref that you pass to useClickOutside. The hook checks DOM containment, and a portal renders outside the parent tree, so a click inside the portal would otherwise look like 'outside' to the parent ref.