BEGINNERREACTHOOKS

useClickOutside Hook for React

A typed React hook that fires a callback when the user clicks outside a referenced element. Perfect for closing dropdowns and modals.

Published May 18, 2026
reacthooksreftypescriptui

useClickOutside is a React hook that calls a callback whenever the user clicks anywhere outside a referenced element. This is the standard pattern for closing dropdowns, modals, popovers, and tooltips when the user clicks away. Listens on mousedown so it fires before any inner click handler.

Tested on React 19, modern browsers.

When to Use This

  • Closing a dropdown menu when the user clicks outside it
  • Dismissing a modal or sheet on backdrop click
  • Hiding a tooltip when focus moves elsewhere
  • Auto-collapsing an expanded card

Don't use this when the element is rendered through a portal that the ref doesn't cover (use a custom contains check) or when you need to detect clicks on a specific element only (use a regular click handler).

Code

import { useEffect, type RefObject } from 'react';
 
export function useClickOutside<T extends HTMLElement>(
  ref: RefObject<T | null>,
  handler: (event: MouseEvent | TouchEvent) => void
): void {
  useEffect(() => {
    function listener(event: MouseEvent | TouchEvent) {
      const el = ref.current;
      if (!el || el.contains(event.target as Node)) {
        return;
      }
      handler(event);
    }
 
    document.addEventListener('mousedown', listener);
    document.addEventListener('touchstart', listener);
 
    return () => {
      document.removeEventListener('mousedown', listener);
      document.removeEventListener('touchstart', listener);
    };
  }, [ref, handler]);
}

The hook is generic over the element type, so you get full TypeScript inference when you pass a ref. mousedown and touchstart cover both desktop and mobile interaction. el.contains is the DOM API for "is this node inside this element?".

Usage

A dropdown menu that closes when the user clicks outside it:

'use client';
 
import { useRef, useState } from 'react';
import { useClickOutside } from '@/hooks/useClickOutside';
 
export function UserMenu() {
  const [open, setOpen] = useState(false);
  const ref = useRef<HTMLDivElement>(null);
 
  useClickOutside(ref, () => setOpen(false));
 
  return (
    <div ref={ref} className="relative">
      <button onClick={() => setOpen((prev) => !prev)}>
        Menu
      </button>
      {open && (
        <div className="absolute top-full mt-2 bg-white shadow-lg rounded p-2">
          <button>Profile</button>
          <button>Settings</button>
          <button>Sign out</button>
        </div>
      )}
    </div>
  );
}

The wrapping div is the boundary. Clicks inside it (including the trigger button and menu items) keep the menu open. Clicks anywhere else close it.

Things to Know

  • The handler should be stable. Pass it via useCallback or define it inline only if you don't mind the effect re-running on every render. The latter is fine for simple setOpen(false) patterns but wasteful for heavy callbacks.
  • mousedown versus click matters. With click, child button handlers run first, then the outside handler closes the dropdown. With mousedown, the dropdown closes before the click event ever fires. Pick mousedown for "click-away" semantics, click for "click-on-the-action" semantics.
  • Portals break the contains check. If your menu renders into document.body via createPortal, the portal's content is not a descendant of your ref. You need a second ref that covers the portal, or use event.composedPath() for shadow DOM compatibility.
  • Touchstart fires before mousedown on mobile. Listening on both is fine — the handler is idempotent if you only call setOpen(false).
  • Don't use this for ESC key dismissal. That's a separate concern. Use useKeyboardShortcut for it.

Frequently Asked Questions

Why listen on mousedown instead of click?

mousedown fires before any click handler runs inside the element, so you can decide whether to close the dropdown before any nested button has a chance to act. Listening on click means a child button click would close the dropdown after the button's handler runs, which often causes layout flicker.

Does useClickOutside work with portal-rendered elements?

Yes, but only if the portal content is also wrapped in a ref that you pass to useClickOutside. The hook checks DOM containment, and a portal renders outside the parent tree, so a click inside the portal would otherwise look like 'outside' to the parent ref.

X (Twitter)LinkedIn