useClickOutside

Close a popover, menu, or inline editor when the pointer lands outside its root.

Installation

npx shadcn@latest add miskibin/chat-components/use-click-outside

Usage

"use client"

import { useRef, useState } from "react"
import { useClickOutside } from "@/hooks/use-click-outside"

export function Palette() {
  const [open, setOpen] = useState(false)
  const ref = useRef<HTMLDivElement>(null)

  useClickOutside(ref, () => setOpen(false), open)

  return (
    <div ref={ref}>
      <button onClick={() => setOpen((value) => !value)}>Search</button>
      {open ? <div role="dialog">…</div> : null}
    </div>
  )
}

Examples

Keep the handler stable

The effect re-subscribes whenever handler changes identity. An inline arrow is fine for a small component; in a hot path wrap it in useCallback.

const close = React.useCallback(() => setOpen(false), [])
useClickOutside(ref, close, open)

Pair it with Escape

Pointer dismissal alone is not enough — give keyboard users the same exit.

useClickOutside(ref, close, open)

React.useEffect(() => {
  if (!open) return
  const onKey = (event: KeyboardEvent) => {
    if (event.key === "Escape") close()
  }
  window.addEventListener("keydown", onKey)
  return () => window.removeEventListener("keydown", onKey)
}, [open, close])

Notes

API reference

useClickOutside(ref, handler, enabled?)

ref*RefObject<T | null>
The root to watch. Anything inside it counts as inside.
handler*() => void
Called on a mousedown outside the root — before focus moves, so a click straight onto another control still works.
enabledboolean = true
Pass your open state so the listener only exists while it matters.