useClickOutside
EditRuns a handler when a pointer event occurs outside supplied elements.
import { useEffect, useLayoutEffect, useRef } from "react";
import type { RefObject } from "react";
export interface ClickOutsideEventMap { click: MouseEvent; mousedown: MouseEvent; pointerdown: PointerEvent; touchstart: TouchEvent;}
export type ClickOutsideEvent = ClickOutsideEventMap[keyof ClickOutsideEventMap];
export type ClickOutsideEventType = keyof ClickOutsideEventMap;
export interface UseClickOutsideOptions< EventType extends ClickOutsideEventType = "pointerdown",> { capture?: boolean; enabled?: boolean; eventType?: EventType;}
const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
/** Calls a handler when an event occurs outside every supplied element. */export const useClickOutside = < ElementType extends Element, EventType extends ClickOutsideEventType = "pointerdown",>( refs: | RefObject<ElementType | null> | readonly RefObject<ElementType | null>[], handler: ( this: void, event: ClickOutsideEventMap[EventType], ) => void, options: UseClickOutsideOptions<EventType> = {},): void => { const refsRef = useRef(refs); const handlerRef = useRef(handler); useIsomorphicLayoutEffect(() => { refsRef.current = refs; handlerRef.current = handler; }, [handler, refs]);
const { capture = true, enabled = true, eventType = "pointerdown", } = options;
useEffect(() => { if (!enabled || typeof document === "undefined") return;
const listener = (event: Event) => { const currentRefs: readonly RefObject<ElementType | null>[] = Array.isArray(refsRef.current) ? refsRef.current : [refsRef.current as RefObject<ElementType | null>]; const elements = currentRefs .map((ref) => ref.current) .filter((element): element is ElementType => element !== null); if (elements.length === 0) return;
const path = event .composedPath() .filter((item): item is EventTarget => item !== undefined); const target = event.target; const isInside = elements.some( (element) => path.includes(element) || (target instanceof Node && element.contains(target)), );
if (!isInside) { const currentHandler = handlerRef.current; currentHandler(event as ClickOutsideEventMap[EventType]); } };
document.addEventListener(eventType, listener, capture); return () => document.removeEventListener(eventType, listener, capture); }, [capture, enabled, eventType]);};Download
Section titled “Download”wget -O src/hooks/useClickOutside.ts https://raw.githubusercontent.com/jrTilak/lazykit/HEAD/registry/react-hooks/useClickOutside.tstemp_dir="$(mktemp -d)" && bunx degit jrTilak/lazykit/registry/react-hooks "$temp_dir" && mv "$temp_dir/useClickOutside.ts" src/hooks/useClickOutside.ts && rm -r "$temp_dir"Examples
Section titled “Examples”import { useRef, useState } from "react";
import { useClickOutside } from "./useClickOutside";
export const ClickOutsideExample = () => { const panelRef = useRef<HTMLDivElement>(null); const [open, setOpen] = useState(true); useClickOutside(panelRef, () => setOpen(false), { enabled: open });
return ( <div ref={panelRef}> <button type="button" onClick={() => setOpen(true)}>Open</button> {open && <p>Click outside this panel to close it.</p>} </div> );};refs— One ref or a readonly list of refs that count as inside.handler— Receives the exact event selected byeventType.options.eventType—"pointerdown"by default; also accepts"click","mousedown", or"touchstart".options.enabled— Enables the listener. Defaults totrue.options.capture— Uses capture-phase listening. Defaults totrue.
Returns
Section titled “Returns”Nothing. Events inside any current element, including composed paths, are ignored.