Sitelet https://usereact.org/functions/useClickOutside
Skip to content

useClickOutside() ​

Category
Export Size
1.0 kB
Gzipped
433 B
SSR
SSR support

Live demo: useClickOutside

Close a panel when clicks happen outside multiple protected refs (toggle button + content area).

Loading demo...

Demo sourceJSX
JSX

Overview ​

useClickOutside registers global mousedown and touchstart listeners and calls your handler when the event target is outside every referenced DOM node (supports either a single ref or an array of refs, which is handy when the “inside” region is split across a toggle button and a floating panel). It keeps the latest handler in a ref so you do not need to rebind listeners on every render, and it no-ops when document is unavailable (SSR).

What it accepts ​

  • refs: ElementRef.
  • handler: (event: MouseEvent | TouchEvent) => void.

What it returns ​

  • Nothing (void). Side effects only.

Usage ​

Real-world example: a popover menu that closes on outside clicks, while clicks on the toggle button still count as “inside”.

tsx
import { useRef, useState } from 'react'
import useClickOutside from '@dedalik/use-react/useClickOutside'

function Example() {
  const [open, setOpen] = useState(false)
  const toggleRef = useRef<HTMLButtonElement | null>(null)
  const panelRef = useRef<HTMLDivElement | null>(null)

  useClickOutside([toggleRef, panelRef], () => setOpen(false))

  return (
    <div>
      <h3>Account menu</h3>
      <button ref={toggleRef} type='button' onClick={() => setOpen((v) => !v)}>
        {open ? 'Close menu' : 'Open menu'}
      </button>

      {open ? (
        <div
          ref={panelRef}
          style={{
            marginTop: 8,
            border: '1px solid #ddd',
            borderRadius: 8,
            padding: 12,
            width: 240,
          }}
        >
          <p>Click outside the button or panel to close.</p>
          <button type='button' onClick={() => setOpen(false)}>
            Log out (demo)
          </button>
        </div>
      ) : null}
    </div>
  )
}

export default function Demo() {
  return <Example />
}

API Reference ​

useClickOutside ​

Signature: useClickOutside(refs: ElementRef, handler: (event: MouseEvent | TouchEvent) => void)

Parameters ​

  1. refs (ElementRef) - One ref or an array of refs whose combined DOM subtree should be treated as “inside”.
  2. handler ((event: MouseEvent | TouchEvent) => void) - Called when a pointer event happens outside all refs.

Returns ​

Nothing (void). The hook registers listeners only.

Copy-paste hook ​

TypeScript ​

tsx
import { RefObject, useEffect, useRef } from 'react'

type ElementRef = RefObject<HTMLElement | null> | RefObject<HTMLElement | null>[]

export default function useClickOutside(refs: ElementRef, handler: (event: MouseEvent | TouchEvent) => void) {
  const handlerRef = useRef(handler)

  useEffect(() => {
    handlerRef.current = handler
  }, [handler])

  useEffect(() => {
    if (typeof document === 'undefined') return
    const refList = Array.isArray(refs) ? refs : [refs]

    const onPointer = (event: MouseEvent | TouchEvent) => {
      const target = event.target as Node
      const isInside = refList.some((ref) => ref.current?.contains(target))
      if (!isInside) {
        handlerRef.current(event)
      }
    }

    document.addEventListener('mousedown', onPointer)
    document.addEventListener('touchstart', onPointer)

    return () => {
      document.removeEventListener('mousedown', onPointer)
      document.removeEventListener('touchstart', onPointer)
    }
  }, [refs])
}

JavaScript ​

js
import { useEffect, useRef } from 'react'

export default function useClickOutside(refs, handler) {
  const handlerRef = useRef(handler)

  useEffect(() => {
    handlerRef.current = handler
  }, [handler])

  useEffect(() => {
    if (typeof document === 'undefined') return
    const refList = Array.isArray(refs) ? refs : [refs]

    const onPointer = (event) => {
      const target = event.target
      const isInside = refList.some((ref) => ref.current?.contains(target))
      if (!isInside) {
        handlerRef.current(event)
      }
    }

    document.addEventListener('mousedown', onPointer)
    document.addEventListener('touchstart', onPointer)

    return () => {
      document.removeEventListener('mousedown', onPointer)
      document.removeEventListener('touchstart', onPointer)
    }
  }, [refs])
}

Released under the MIT License.