useOutsideClick
A custom React hook that detects clicks outside of a specified element. Useful for implementing dropdowns, modals, popovers, and other UI components that should close when the user clicks outside of them.
Hook Signature
const useOutsideClick = (
ref : React . RefObject < HTMLDivElement >,
callback : Function
) => void
Parameters
ref
React.RefObject<HTMLDivElement>
required
A React ref object attached to the element you want to monitor. The hook will detect clicks outside of this element. Created using useRef<HTMLDivElement>(null).
A function to execute when a click outside the referenced element is detected. The callback receives the event object as an argument. Type : (event: MouseEvent | TouchEvent) => void
Return Value
This hook does not return any value. It sets up event listeners that trigger the callback when appropriate.
Usage Examples
Basic Modal Example
import React , { useRef , useState } from 'react' ;
import { useOutsideClick } from '@/app/hooks/useOutsideClick' ;
const Modal : React . FC = () => {
const [ isOpen , setIsOpen ] = useState ( false );
const modalRef = useRef < HTMLDivElement >( null );
// Close modal when clicking outside
useOutsideClick ( modalRef , () => {
setIsOpen ( false );
});
if ( ! isOpen ) return null ;
return (
< div className = "fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center" >
< div ref = { modalRef } className = "bg-white p-6 rounded-lg shadow-xl" >
< h2 > Modal Content </ h2 >
< p > Click outside to close </ p >
</ div >
</ div >
);
};
import React , { useRef , useState } from 'react' ;
import { useOutsideClick } from '@/app/hooks/useOutsideClick' ;
const Dropdown : React . FC = () => {
const [ isOpen , setIsOpen ] = useState ( false );
const dropdownRef = useRef < HTMLDivElement >( null );
useOutsideClick ( dropdownRef , () => {
if ( isOpen ) {
setIsOpen ( false );
}
});
return (
< div ref = { dropdownRef } className = "relative" >
< button
onClick = {() => setIsOpen (! isOpen )}
className = "px-4 py-2 bg-blue-500 text-white rounded"
>
Toggle Dropdown
</ button >
{ isOpen && (
< div className = "absolute mt-2 w-48 bg-white rounded 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 >
);
};
Popover Example
import React , { useRef , useState } from 'react' ;
import { useOutsideClick } from '@/app/hooks/useOutsideClick' ;
const Popover : React . FC = () => {
const [ isVisible , setIsVisible ] = useState ( false );
const popoverRef = useRef < HTMLDivElement >( null );
useOutsideClick ( popoverRef , ( event ) => {
console . log ( 'Clicked outside:' , event . target );
setIsVisible ( false );
});
return (
< div className = "relative inline-block" >
< button
onClick = {() => setIsVisible ( true )}
className = "px-4 py-2 bg-green-500 text-white rounded"
>
Show Info
</ button >
{ isVisible && (
< div
ref = { popoverRef }
className = "absolute z-10 mt-2 p-4 bg-white border rounded shadow-lg"
>
< p className = "text-sm" > This is a popover with additional information . </ p >
</ div >
)}
</ div >
);
};
Advanced: Multiple Refs
import React , { useRef , useState } from 'react' ;
import { useOutsideClick } from '@/app/hooks/useOutsideClick' ;
const ComplexComponent : React . FC = () => {
const [ activePanel , setActivePanel ] = useState < string | null >( null );
const panel1Ref = useRef < HTMLDivElement >( null );
const panel2Ref = useRef < HTMLDivElement >( null );
// Handle panel 1
useOutsideClick ( panel1Ref , () => {
if ( activePanel === 'panel1' ) {
setActivePanel ( null );
}
});
// Handle panel 2
useOutsideClick ( panel2Ref , () => {
if ( activePanel === 'panel2' ) {
setActivePanel ( null );
}
});
return (
< div className = "flex gap-4" >
< div ref = { panel1Ref } >
< button onClick = {() => setActivePanel ( 'panel1' )} > Open Panel 1 </ button >
{ activePanel === ' panel1 ' && < div > Panel 1 Content </ div >}
</ div >
< div ref = { panel2Ref } >
< button onClick = {() => setActivePanel ( 'panel2' )} > Open Panel 2 </ button >
{ activePanel === ' panel2 ' && < div > Panel 2 Content </ div >}
</ div >
</ div >
);
};
Implementation Details
import React , { useEffect } from "react" ;
export const useOutsideClick = (
ref : React . RefObject < HTMLDivElement >,
callback : Function
) => {
useEffect (() => {
const listener = ( event : any ) => {
// Do nothing if clicking ref's element or descendent elements
if ( ! ref . current || ref . current . contains ( event . target )) {
return ;
}
callback ( event );
};
document . addEventListener ( "mousedown" , listener );
document . addEventListener ( "touchstart" , listener );
return () => {
document . removeEventListener ( "mousedown" , listener );
document . removeEventListener ( "touchstart" , listener );
};
}, [ ref , callback ]);
};
How It Works
Show Event Listener Logic
Setup Phase : When the component mounts, the hook adds event listeners for both mousedown and touchstart events to the document.
Event Handling : When a click/touch occurs anywhere on the page:
If the ref is not yet assigned (!ref.current), do nothing
If the click target is inside the ref element (ref.current.contains(event.target)), do nothing
Otherwise, trigger the callback function
Cleanup Phase : When the component unmounts or the dependencies change, the hook removes the event listeners to prevent memory leaks.
Mobile Support : By listening to both mousedown and touchstart, the hook works on both desktop and mobile devices.
Important Notes
Show Best Practices & Considerations
Event Type : The hook uses mousedown instead of click to detect interactions before the click completes, providing a more responsive feel.
Mobile Support : The touchstart event ensures the hook works on touch devices.
Ref Initialization : Always initialize your ref with null: useRef<HTMLDivElement>(null)
Callback Dependencies : Be mindful of callback function references. If the callback changes on every render, consider using useCallback to memoize it:
const handleOutsideClick = useCallback (() => {
setIsOpen ( false );
}, []);
useOutsideClick ( ref , handleOutsideClick );
Conditional Logic : You can add conditional logic inside the callback to control when the action should occur:
useOutsideClick ( ref , () => {
if ( isOpen ) {
setIsOpen ( false );
}
});
Performance : The hook is lightweight and adds minimal performance overhead. Event listeners are properly cleaned up on unmount.
Common Use Cases
Modal dialogs that close on backdrop click
Dropdown menus and select components
Context menus and right-click menus
Popovers and tooltips
Date pickers and time selectors
Autocomplete suggestion lists
Custom select dropdowns
Mobile navigation menus
Color pickers and other picker components