Build a useClickOutside Hook in React

Build a useClickOutside Hook that detects pointer presses outside an element, so a menu, popover, or dropdown can close itself.

6 min read

A useClickOutside Hook runs a callback when the user presses outside a given element. It is the standard way to close a menu, popover, or dropdown when the pointer leaves it, and it works with mouse, touch, and pen. The callback runs on the press rather than on release, so the menu closes at the moment the user interacts outside it.

Build the Hook

The Hook takes a ref and a callback, then listens on the document so presses anywhere can be checked against the element.

App.jsxApp.jsx
import { useEffect } from "react";
function useClickOutside(ref, onOutside) {
  useEffect(() => {
    function handlePointerDown(event) {
      if (ref.current && !ref.current.contains(event.target)) onOutside();
    }
    document.addEventListener("pointerdown", handlePointerDown);
    return () => document.removeEventListener("pointerdown", handlePointerDown);
  }, [ref, onOutside]);
}

ref.current is the element to protect. The listener stays attached even while the element is unmounted, such as a closed menu, so the ref.current check skips the callback instead of throwing when there is nothing to compare against. contains returns true when the press landed on the element or any descendant, so only genuine outside presses reach onOutside.

The cleanup removes the listener. Keep onOutside stable, or the Effect re-subscribes on every render. Passing an inline arrow changes the callback on each render and re-attaches the listener each time.

Use it for a dropdown

Attach the ref to the panel that appears, and call the Hook from the component that owns the open state.

App.jsxApp.jsx
import { useCallback, useRef, useState } from "react";
 
function Dropdown() {
  const [open, setOpen] = useState(false);
  const menuRef = useRef(null);
  const close = useCallback(() => setOpen(false), []);
  useClickOutside(menuRef, close);
  return (
    <div>
      <button onClick={() => setOpen(!open)}>Menu</button>
      {open && <div ref={menuRef}>Options</div>}
    </div>
  );
}

close keeps the same identity across renders, so the Effect inside useClickOutside subscribes once instead of removing and re-adding the listener on every render. Clicking the button toggles the menu. A press anywhere outside menuRef runs close, so the menu closes.

A press inside the menu does nothing, and a press on the button toggles it as expected. The ref points at the open menu only while it exists, so the check is skipped when the menu is closed. useClickOutside closes it without adding a click handler to every other element on the page.

Keep the interaction accessible

Outside click alone does not cover keyboard users or screen readers. pointerdown fires before a click and works for touch and pen too, which is why the Hook uses it.

  • Also close on Escape and return focus to the trigger.
  • For a true modal, prefer a native dialog element or proper ARIA role.
  • Keep the callback stable, or the Effect re-subscribes on every render.

A dismissible menu still needs a visible trigger, a focus target, and a way to reopen it. Outside click is one piece of that, not the whole pattern.

Why pointerdown instead of click

pointerdown fires as soon as the pointer presses, before focus or layout changes can move the target. It also covers touch and pen, where a click event may not behave the same way. For closing menus, pressing is the right moment because the user has already started to interact outside.

See how to create a custom Hook for the extraction steps, and how to build an accessible React modal with portals for the fuller dialog pattern.

Rune AI

Rune AI

Key Insights

  • Attach the ref to the element that should stay open.
  • Listen for pointerdown on document and check ref.current.contains(event.target).
  • Remove the listener in the Effect cleanup.
  • Keep the callback stable to avoid re-subscribing every render.
  • Pair outside click with Escape and focus return.
RunePowered by Rune AI

Frequently Asked Questions

Why listen for pointerdown instead of click?

pointerdown fires for mouse, touch, and pen as soon as the press starts. It also fires before the element under the pointer changes, which avoids timing edge cases.

What does contains return when the target is the element itself?

Node.contains is inclusive, so an element contains itself. A press on the element counts as inside, and the callback does not run.

Should outside click also close on the Escape key?

Yes. Outside click alone does not cover keyboard users. Close on Escape and move focus back to the trigger for an accessible interaction.

Conclusion

A useClickOutside Hook attaches a document-level pointerdown listener and runs a callback when the press lands outside the given ref. Combine it with Escape handling and focus management to keep menus and popovers accessible.