npm.io
0.7.0 • Published 22h ago

@usefy/use-on-click-outside

Licence
MIT
Version
0.7.0
Deps
0
Vulns
0
Weekly
0
Stars
2

usefy logo

@usefy/use-on-click-outside

A React hook for detecting clicks outside of specified elements

npm version npm downloads bundle size license

InstallationQuick StartAPI ReferenceExamplesLicense

View Storybook Demo


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

View Detailed Coverage Report (GitHub Pages)

Test Categories
Basic Functionality Tests
  • 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)
Multiple Refs Tests
  • Support array of refs
  • Not call handler when clicking inside any of the refs
  • Call handler only when clicking outside all refs
Enabled Option Tests
  • Not call handler when enabled is false
  • Not register event listener when disabled
  • Toggle listener when enabled changes
  • Default enabled to true
Exclude Refs Tests
  • Not call handler when clicking on excluded element
  • Support multiple exclude refs
  • Handle dynamically added exclude refs
shouldExclude Function Tests
  • Not call handler when shouldExclude returns true
  • Pass correct target to shouldExclude function
Touch Events Tests
  • Call handler on touchstart when detectTouch is true
  • Not listen for touch events when detectTouch is false
Handler Stability Tests
  • Not re-register listener when handler changes
  • Call the latest handler after update
Cleanup Tests
  • Remove event listeners on unmount
  • Not call handler after unmount

License

MIT mirunamu

This package is part of the usefy monorepo.


Built with care by the usefy team