npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@usefy/use-on-click-outside

v0.0.19

Published

A React hook for detecting clicks outside of specified elements

Readme


Overview

@usefy/use-on-click-outside detects clicks outside of specified element(s) and calls your handler. Perfect for closing modals, dropdowns, popovers, tooltips, and any UI component that should dismiss when clicking elsewhere.

Part of the @usefy ecosystem — a collection of production-ready React hooks designed for modern applications.

Why use-on-click-outside?

  • Zero Dependencies — Pure React implementation with no external dependencies
  • TypeScript First — Full type safety with exported interfaces
  • Multiple Refs Support — Pass a single ref or an array of refs (e.g., button + dropdown)
  • Exclude Elements — Exclude specific elements from triggering the handler via excludeRefs or shouldExclude
  • Mouse + Touch Support — Handles both mousedown and touchstart events for mobile compatibility
  • Capture Phase — Uses capture phase by default to avoid stopPropagation issues
  • Conditional Activation — Enable/disable via the enabled option
  • Handler Stability — No re-registration when handler changes
  • SSR Compatible — Works seamlessly with Next.js, Remix, and other SSR frameworks
  • Well Tested — 97.61% test coverage with Vitest

Installation

# npm
npm install @usefy/use-on-click-outside

# yarn
yarn add @usefy/use-on-click-outside

# pnpm
pnpm add @usefy/use-on-click-outside

Peer Dependencies

This package requires React 18 or 19:

{
  "peerDependencies": {
    "react": "^18.0.0 || ^19.0.0"
  }
}

Quick Start

import { useOnClickOutside } from "@usefy/use-on-click-outside";
import { useRef, useState } from "react";

function Modal({ onClose }: { onClose: () => void }) {
  const modalRef = useRef<HTMLDivElement>(null);

  useOnClickOutside(modalRef, () => onClose());

  return (
    <div className="overlay">
      <div ref={modalRef} className="modal">
        Click outside to close
      </div>
    </div>
  );
}

API Reference

useOnClickOutside(ref, handler, options?)

A hook that detects clicks outside of specified element(s).

Parameters

| Parameter | Type | Description | | --------- | -------------------------- | --------------------------------------------------------- | | ref | RefTarget<T> | Single ref or array of refs to detect outside clicks for | | handler | OnClickOutsideHandler | Callback function called when a click outside is detected | | options | UseOnClickOutsideOptions | Configuration options |

Options

| Option | Type | Default | Description | | ---------------- | ----------------------------------- | -------------- | --------------------------------------------------------- | | enabled | boolean | true | Whether the event listener is active | | capture | boolean | true | Use event capture phase (immune to stopPropagation) | | eventType | MouseEventType | "mousedown" | Mouse event type to listen for | | touchEventType | TouchEventType | "touchstart" | Touch event type to listen for | | detectTouch | boolean | true | Whether to detect touch events (mobile support) | | excludeRefs | RefObject<HTMLElement>[] | [] | Refs to exclude from outside click detection | | shouldExclude | (target: Node) => boolean | undefined | Custom function to determine if target should be excluded | | eventTarget | Document \| HTMLElement \| Window | document | The event target to attach listeners to |

Types

type ClickOutsideEvent = MouseEvent | TouchEvent;
type OnClickOutsideHandler = (event: ClickOutsideEvent) => void;
type MouseEventType =
  | "mousedown"
  | "mouseup"
  | "click"
  | "pointerdown"
  | "pointerup";
type TouchEventType = "touchstart" | "touchend";
type RefTarget<T extends HTMLElement> =
  | React.RefObject<T | null>
  | Array<React.RefObject<HTMLElement | null>>;

Returns

void


Examples

Basic Modal

import { useOnClickOutside } from "@usefy/use-on-click-outside";
import { useRef, useState } from "react";

function Modal({ isOpen, onClose, children }: ModalProps) {
  const modalRef = useRef<HTMLDivElement>(null);

  useOnClickOutside(modalRef, onClose, { enabled: isOpen });

  if (!isOpen) return null;

  return (
    <div className="overlay">
      <div ref={modalRef} className="modal">
        {children}
      </div>
    </div>
  );
}

Dropdown with Multiple Refs

When you have a button that toggles a dropdown, you want clicks on both the button and dropdown to be considered "inside":

import { useOnClickOutside } from "@usefy/use-on-click-outside";
import { useRef, useState } from "react";

function Dropdown() {
  const [isOpen, setIsOpen] = useState(false);
  const buttonRef = useRef<HTMLButtonElement>(null);
  const menuRef = useRef<HTMLDivElement>(null);

  // Both button and menu are considered "inside"
  useOnClickOutside([buttonRef, menuRef], () => setIsOpen(false), {
    enabled: isOpen,
  });

  return (
    <>
      <button ref={buttonRef} onClick={() => setIsOpen(!isOpen)}>
        Toggle Menu
      </button>
      {isOpen && (
        <div ref={menuRef} className="dropdown-menu">
          <button>Option 1</button>
          <button>Option 2</button>
          <button>Option 3</button>
        </div>
      )}
    </>
  );
}

Exclude Specific Elements

Use excludeRefs to prevent certain elements from triggering the outside click handler:

import { useOnClickOutside } from "@usefy/use-on-click-outside";
import { useRef, useState } from "react";

function ModalWithToast() {
  const [isOpen, setIsOpen] = useState(false);
  const modalRef = useRef<HTMLDivElement>(null);
  const toastRef = useRef<HTMLDivElement>(null);

  useOnClickOutside(modalRef, () => setIsOpen(false), {
    enabled: isOpen,
    excludeRefs: [toastRef], // Clicks on toast won't close modal
  });

  return (
    <>
      {isOpen && (
        <div className="overlay">
          <div ref={modalRef} className="modal">
            Modal Content
          </div>
        </div>
      )}
      <div ref={toastRef} className="toast">
        This toast won't close the modal when clicked
      </div>
    </>
  );
}

Custom Exclude Logic with shouldExclude

Use shouldExclude for dynamic exclusion based on element properties:

import { useOnClickOutside } from "@usefy/use-on-click-outside";
import { useRef, useState } from "react";

function MenuWithIgnoredElements() {
  const [isOpen, setIsOpen] = useState(false);
  const menuRef = useRef<HTMLDivElement>(null);

  useOnClickOutside(menuRef, () => setIsOpen(false), {
    enabled: isOpen,
    shouldExclude: (target) => {
      // Ignore clicks on elements with specific class
      return (target as Element).closest?.(".ignore-outside-click") !== null;
    },
  });

  return (
    <>
      {isOpen && (
        <div ref={menuRef} className="menu">
          Menu Content
        </div>
      )}
      <button className="ignore-outside-click">
        This button won't close the menu
      </button>
    </>
  );
}

Popover with Touch Support

Touch events are enabled by default for mobile compatibility:

import { useOnClickOutside } from "@usefy/use-on-click-outside";
import { useRef, useState } from "react";

function Popover({ trigger, content }: PopoverProps) {
  const [isOpen, setIsOpen] = useState(false);
  const popoverRef = useRef<HTMLDivElement>(null);

  // Handles both mouse and touch events
  useOnClickOutside(popoverRef, () => setIsOpen(false), {
    enabled: isOpen,
    detectTouch: true, // default
  });

  return (
    <div ref={popoverRef}>
      <button onClick={() => setIsOpen(!isOpen)}>{trigger}</button>
      {isOpen && <div className="popover-content">{content}</div>}
    </div>
  );
}

Context Menu

import { useOnClickOutside } from "@usefy/use-on-click-outside";
import { useRef, useState } from "react";

function ContextMenu() {
  const [menu, setMenu] = useState<{ x: number; y: number } | null>(null);
  const menuRef = useRef<HTMLDivElement>(null);

  useOnClickOutside(menuRef, () => setMenu(null), {
    enabled: menu !== null,
  });

  const handleContextMenu = (e: React.MouseEvent) => {
    e.preventDefault();
    setMenu({ x: e.clientX, y: e.clientY });
  };

  return (
    <div onContextMenu={handleContextMenu} className="context-area">
      Right-click anywhere
      {menu && (
        <div
          ref={menuRef}
          className="context-menu"
          style={{ position: "fixed", left: menu.x, top: menu.y }}
        >
          <button>Cut</button>
          <button>Copy</button>
          <button>Paste</button>
        </div>
      )}
    </div>
  );
}

Different Event Types

You can customize which mouse/touch events trigger the handler:

import { useOnClickOutside } from "@usefy/use-on-click-outside";

// Use 'click' instead of 'mousedown' (fires after full click completes)
useOnClickOutside(ref, handler, {
  eventType: "click",
});

// Use 'touchend' instead of 'touchstart'
useOnClickOutside(ref, handler, {
  touchEventType: "touchend",
});

// Disable touch detection entirely
useOnClickOutside(ref, handler, {
  detectTouch: false,
});

TypeScript

This hook is written in TypeScript with exported types.

import {
  useOnClickOutside,
  type UseOnClickOutsideOptions,
  type OnClickOutsideHandler,
  type ClickOutsideEvent,
  type RefTarget,
  type MouseEventType,
  type TouchEventType,
} from "@usefy/use-on-click-outside";

// Handler type
const handleOutsideClick: OnClickOutsideHandler = (event) => {
  console.log("Clicked outside at:", event.clientX, event.clientY);
};

// Options type
const options: UseOnClickOutsideOptions = {
  enabled: true,
  capture: true,
  eventType: "mousedown",
  detectTouch: true,
};

useOnClickOutside(ref, handleOutsideClick, options);

Testing

This package maintains comprehensive test coverage to ensure reliability and stability.

Test Coverage

| Category | Coverage | | ---------- | -------------- | | Statements | 97.61% (41/42) | | Branches | 93.93% (31/33) | | Functions | 100% (7/7) | | Lines | 97.61% (41/42) |

Test Categories

  • Call handler when clicked outside the element
  • Not call handler when clicked inside the element
  • Not call handler when target is not connected to DOM
  • Handle events with correct type (MouseEvent/TouchEvent)
  • Support array of refs
  • Not call handler when clicking inside any of the refs
  • Call handler only when clicking outside all refs
  • Not call handler when enabled is false
  • Not register event listener when disabled
  • Toggle listener when enabled changes
  • Default enabled to true
  • Not call handler when clicking on excluded element
  • Support multiple exclude refs
  • Handle dynamically added exclude refs
  • Not call handler when shouldExclude returns true
  • Pass correct target to shouldExclude function
  • Call handler on touchstart when detectTouch is true
  • Not listen for touch events when detectTouch is false
  • Not re-register listener when handler changes
  • Call the latest handler after update
  • Remove event listeners on unmount
  • Not call handler after unmount

Running Tests

# Run all tests
pnpm test

# Run tests in watch mode
pnpm test:watch

# Run tests with coverage report
pnpm test --coverage

License

MIT © mirunamu

This package is part of the usefy monorepo.