This can be useful for closing a modal, a dropdown menu etc.
Pass a single RefObject or an array of refs. A click counts as outside only when at least one ref has a non-null current; if every ref is still unmounted (current === null), the handler does not run. For multiple DOM roots (e.g. floating panel + trigger), pass both refs in an array instead of merging refs, unless you already use a mergeRefs helper that produces one callback ref.
Usage
import { useRef } from 'react'
import { useOnClickOutside } from './useOnClickOutside'
export default function Component() {
const primaryRef = useRef<HTMLButtonElement>(null)
const secondaryRef = useRef<HTMLDivElement>(null)
const handleClickOutside = () => {
console.log('clicked outside both regions')
}
const handleClickInside = () => {
console.log('clicked primary')
}
useOnClickOutside([primaryRef, secondaryRef], handleClickOutside)
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<button
ref={primaryRef}
onClick={handleClickInside}
style={{ width: 200, height: 80, background: 'cyan' }}
type="button"
>
Primary (inside)
</button>
<div ref={secondaryRef} style={{ width: 200, height: 80, background: 'lightyellow' }}>
Secondary region (inside)
</div>
</div>
)
}
API
function useOnClickOutside(ref: RefObject<T | null> | RefObject<T | null>[], handler: (event: MouseEvent | TouchEvent | FocusEvent) => void, eventType?: EventType, eventListenerOptions?: AddEventListenerOptions): void
Custom hook that handles clicks outside a specified element.
Parameters
| Name | Type | Default value | Description |
|---|---|---|---|
ref | `RefObject<T | null> | RefObject<T |
handler | `(event: MouseEvent | TouchEvent | FocusEvent) => void` |
eventType? | EventType | 'mousedown' | The mouse event type to listen for (optional, default is 'mousedown'). |
eventListenerOptions? | AddEventListenerOptions | ... | The options object to be passed to the addEventListener method (optional). |
Returns
No return description available yet.
Type declaration
EventType
Supported event types.
Hook
import type { RefObject } from 'react'
import { useEventListener } from '../useEventListener'
/** Supported event types. */
export type EventType = 'mousedown' | 'mouseup' | 'touchstart' | 'touchend' | 'focusin' | 'focusout'
/**
* Custom hook that handles clicks outside a specified element.
* @template T - The type of the element's reference.
* @param {RefObject<T | null> | RefObject<T | null>[]} ref - The React ref object(s) representing the element(s) to watch for outside clicks.
* @param {(event: MouseEvent | TouchEvent | FocusEvent) => void} handler - The callback function to be executed when a click outside the element occurs.
* @param {EventType} [eventType] - The mouse event type to listen for (optional, default is 'mousedown').
* @param {?AddEventListenerOptions} [eventListenerOptions] - The options object to be passed to the `addEventListener` method (optional).
* @returns {void}
* @public
* @see [Documentation](https://usehooks-ts.com/react-hook/use-on-click-outside)
* @example
* ```tsx
* const ref = useRef<HTMLDivElement>(null);
* useOnClickOutside(ref, () => {
* // Outside the mounted element (ref.current must be non-null to define “inside”).
* });
* ```
* @example
* ```tsx
* const a = useRef<HTMLDivElement>(null);
* const b = useRef<HTMLDivElement>(null);
* useOnClickOutside([a, b], () => {
* // Outside both regions; ignored while every ref’s current is null.
* });
* ```
*/
export function useOnClickOutside<T extends HTMLElement = HTMLElement>(
ref: RefObject<T | null> | RefObject<T | null>[],
handler: (event: MouseEvent | TouchEvent | FocusEvent) => void,
eventType: EventType = 'mousedown',
eventListenerOptions: AddEventListenerOptions = { capture: true },
): void {
useEventListener(
eventType,
event => {
const target = event.target as Node
// Do nothing if the target is not connected element with document
if (!target || !target.isConnected) {
return
}
let isOutside: boolean
if (Array.isArray(ref)) {
const mounted = ref.filter((r): r is RefObject<T> & { current: T } => r.current != null)
isOutside = mounted.length > 0 && mounted.every(r => !r.current.contains(target))
} else {
isOutside = Boolean(ref.current && !ref.current.contains(target))
}
if (isOutside) {
handler(event)
}
},
undefined,
eventListenerOptions,
)
}