react-cool-onclickoutside
A tiny React hook that fires a callback when users click or touch outside one or more registered elements.
Repository Health
Technical Analysis
react-cool-onclickoutside is a single-purpose React hook for the classic “dismiss on outside click” pattern used by dropdowns, modals, tooltips, and popovers. Instead of wiring up document-level event listeners by hand in every component, you call the hook, attach the callback ref it returns, and it manages subscription, cleanup, and a handful of tricky edge cases for you.
Beyond the basic case, it supports registering multiple refs under one callback (useful when a trigger and its floating panel live in separate DOM subtrees), excluding specific elements via a CSS ignore-class, ignoring clicks on the browser’s own scrollbar, and detecting clicks that land inside an <iframe> — something a plain document click listener can’t see because iframes are a separate browsing context. It ships as a single dependency-free hook (aside from the React peer dependency) with full TypeScript typings and a sub-1kB gzipped footprint.
What You Get
- A single
useOnclickOutsidehook returning a callback ref, with full TypeScript typings included - Multi-ref support for guarding several disconnected DOM elements with one shared callback
- Passive touch listeners, scrollbar exclusion, and CSS-class-based ignore rules for edge cases
- Iframe click detection via the window
blurevent, with an option to disable it
Common Use Cases
- Closing a dropdown or select menu when the user clicks elsewhere
- Dismissing a modal, popover, or tooltip on outside interaction
- Coordinating outside-click behavior across a trigger and a portal-rendered panel
- Detecting clicks that land inside a third-party iframe embed
Under The Hood
Architecture
The whole implementation lives in one hook file, src/index.ts, plus a small helper module src/canUsePassiveEvents.ts. useOnclickOutside takes a callback and an options object and returns a callback ref that accumulates registered elements into React state (refsState) via useCallback; its effect attaches document-level mousedown/touchstart listeners (configurable through eventTypes) plus, when detectIFrame is true, a window blur listener that checks document.activeElement on a setTimeout to catch clicks landing inside <iframe> elements, which never bubble to document. The handler closure walks up from event.target via hasIgnoreClass to honor the ignoreClass opt-out, checks clickedOnScrollbar against document.documentElement’s dimensions when excludeScrollbar is set, and invokes the latest callback through a ref (callbackRef) to avoid stale closures without re-subscribing listeners on every render. There is no internal layering beyond this — it is intentionally a flat, single-function library, so any change to the core hook ripples directly to every consumer rather than being absorbed by an abstraction layer.
Tech Stack
Written in TypeScript, targeting React >=16.8.0 as a peer dependency with no other runtime dependencies. Built with Rollup and a set of @rollup/plugin-* packages plus rollup-plugin-terser and rollup-plugin-size-snapshot, transpiled via Babel presets for env/React/TypeScript, and published as both CJS and ESM bundles with generated .d.ts typings. Testing runs on Jest with ts-jest, @testing-library/react, and react-test-renderer. Linting and formatting go through ESLint (a shared eslint-config-welly config) and Prettier, wired together with lint-staged and Husky git hooks. A companion demo app under app/ hosts the live Netlify playground referenced from the README.
Code Quality
Tests live in src/__tests__/ and are comprehensive for the surface area involved — the main suite exercises multi-ref registration, both string and array forms of ignoreClass, dynamically-added refs, custom eventTypes, the disabled toggle, scrollbar exclusion, and several distinct iframe focus/blur scenarios, with coverage tracked via a Coveralls badge. TypeScript types are used throughout with explicit exported interfaces, though a handful of // @ts-ignore and loosely-typed any escape hatches appear where DOM event shapes get awkward. ESLint and Prettier run as pre-commit hooks via lint-staged/Husky, a tsc type-check is part of the lint script, and a CI badge references an automated workflow.
API Design
The public surface is a single default export, useOnclickOutside(callback, options?), returning one callback ref — no provider, no context, no class to instantiate. The common case requires only the standard ref prop with zero extra boilerplate, option names are consistent and self-explanatory (ignoreClass, excludeScrollbar, detectIFrame, disabled), and every option defaults sensibly so edge cases are opt-in additions to the same options object rather than separate APIs. Documentation is unusually thorough for a project this size, with a README that walks through every option using runnable code samples and a linked CodeSandbox demo.