React Higher Order Component that enables components to detect and handle clicks outside their DOM boundaries
npx @tessl/cli install tessl/npm-react-onclickoutside@6.13.0React OnClickOutside is a Higher Order Component (HOC) that enables React components to detect and handle clicks that occur outside their DOM boundaries. It provides a clean, configurable solution for implementing dropdowns, modals, tooltips and other UI components that need to close when users click elsewhere.
npm install react-onclickoutsideimport onClickOutside from "react-onclickoutside";For CommonJS:
const onClickOutside = require("react-onclickoutside").default;Named import for constants:
import onClickOutside, { IGNORE_CLASS_NAME } from "react-onclickoutside";TypeScript imports with types:
import onClickOutside, { IGNORE_CLASS_NAME } from "react-onclickoutside";
// Note: TypeScript types not officially exported, interfaces shown in this doc for referenceimport React, { Component } from "react";
import onClickOutside from "react-onclickoutside";
class Dropdown extends Component {
constructor(props) {
super(props);
this.state = { isOpen: false };
}
toggleDropdown = () => {
this.setState({ isOpen: !this.state.isOpen });
};
handleClickOutside = (event) => {
this.setState({ isOpen: false });
};
render() {
return (
<div>
<button onClick={this.toggleDropdown}>
Menu {this.state.isOpen ? "▲" : "▼"}
</button>
{this.state.isOpen && (
<ul>
<li>Option 1</li>
<li>Option 2</li>
<li>Option 3</li>
</ul>
)}
</div>
);
}
}
export default onClickOutside(Dropdown);For modern React applications using hooks, you may not need this HOC. Here's a functional component approach:
import React, { useEffect, useState, useRef } from "react";
function useClickOutside(handler) {
const ref = useRef(null);
const [listening, setListening] = useState(false);
useEffect(() => {
if (listening) return;
if (!ref.current) return;
setListening(true);
const clickHandler = (event) => {
if (!ref.current.contains(event.target)) {
handler(event);
}
};
['click', 'touchstart'].forEach((type) => {
document.addEventListener(type, clickHandler);
});
return () => {
['click', 'touchstart'].forEach((type) => {
document.removeEventListener(type, clickHandler);
});
setListening(false);
};
}, [handler, listening]);
return ref;
}
const Dropdown = () => {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useClickOutside(() => setIsOpen(false));
return (
<div ref={dropdownRef}>
<button onClick={() => setIsOpen(!isOpen)}>
Menu {isOpen ? "▲" : "▼"}
</button>
{isOpen && (
<ul>
<li>Option 1</li>
<li>Option 2</li>
<li>Option 3</li>
</ul>
)}
</div>
);
};React OnClickOutside uses a Higher Order Component pattern that wraps existing components without requiring significant code changes. Key components:
The main HOC function that wraps React components to add outside click detection.
/**
* Higher Order Component that adds outside click detection to React components
* @param WrappedComponent - React component class or functional component to wrap
* @param config - Optional configuration object
* @returns Enhanced React component with outside click functionality
*/
function onClickOutside<P = {}>(
WrappedComponent: React.ComponentType<P>,
config?: Config
): React.ComponentType<P & EnhancedProps>;
interface Config {
/** Function that returns the click handler for the component instance */
handleClickOutside?: (instance: any) => (event: Event) => void;
/** Default excludeScrollbar setting for all instances (default: false) */
excludeScrollbar?: boolean;
/** Function to determine which DOM node to use for outside click detection */
setClickOutsideRef?: () => (instance: any) => HTMLElement;
}
/** Props automatically added to wrapped components */
interface EnhancedProps {
/** Event types to listen for (default: ["mousedown", "touchstart"]) */
eventTypes?: string[] | string;
/** Whether to ignore clicks on the scrollbar (default: false) */
excludeScrollbar?: boolean;
/** CSS class name to ignore during outside click detection (default: "ignore-react-onclickoutside") */
outsideClickIgnoreClass?: string;
/** Whether to call preventDefault on outside click events (default: false) */
preventDefault?: boolean;
/** Whether to call stopPropagation on outside click events (default: false) */
stopPropagation?: boolean;
/** Whether to disable outside click detection initially (default: false) */
disableOnClickOutside?: boolean;
/** Optional click handler function passed as prop */
handleClickOutside?: (event: Event) => void;
/** Function to enable outside click detection */
enableOnClickOutside?: () => void;
/** Function to disable outside click detection */
disableOnClickOutside?: () => void;
}
/** Default props applied by the HOC */
const DEFAULT_PROPS = {
eventTypes: ['mousedown', 'touchstart'],
excludeScrollbar: false,
outsideClickIgnoreClass: 'ignore-react-onclickoutside',
preventDefault: false,
stopPropagation: false
};Usage with configuration:
import React, { Component } from "react";
import onClickOutside from "react-onclickoutside";
class MyComponent extends Component {
myClickOutsideHandler = (event) => {
console.log("Clicked outside!");
};
render() {
return <div>Content</div>;
}
}
const config = {
handleClickOutside: (instance) => instance.myClickOutsideHandler
};
export default onClickOutside(MyComponent, config);Controls which DOM events trigger outside click detection.
// From EnhancedProps interface above
eventTypes?: string[] | string; // default: ["mousedown", "touchstart"]Usage:
// Single event type
<WrappedComponent eventTypes="click" />
// Multiple event types
<WrappedComponent eventTypes={["click", "touchend"]} />
// Default is ["mousedown", "touchstart"]Controls whether clicks on the browser scrollbar should be ignored.
// From EnhancedProps interface above
excludeScrollbar?: boolean; // default: falseUsage:
<WrappedComponent excludeScrollbar={true} />Controls event prevention and propagation behavior.
// From EnhancedProps interface above
preventDefault?: boolean; // default: false
stopPropagation?: boolean; // default: falseUsage:
<WrappedComponent
preventDefault={true}
stopPropagation={true}
/>Controls which elements should be ignored during outside click detection.
// From EnhancedProps interface above
outsideClickIgnoreClass?: string; // default: "ignore-react-onclickoutside"
/** Default CSS class name for elements to ignore */
export const IGNORE_CLASS_NAME: string = "ignore-react-onclickoutside";Usage:
import onClickOutside, { IGNORE_CLASS_NAME } from "react-onclickoutside";
// Use default ignore class
<div className={IGNORE_CLASS_NAME}>This won't trigger outside click</div>
// Use custom ignore class
<WrappedComponent outsideClickIgnoreClass="my-ignore-class" />
<div className="my-ignore-class">This won't trigger outside click</div>Methods for programmatically enabling/disabling outside click detection.
// Methods available on the HOC wrapper component instance
interface WrapperInstance {
/** Returns reference to the wrapped component instance */
getInstance(): React.Component | any;
/** Explicitly enables outside click event listening */
enableOnClickOutside(): void;
/** Explicitly disables outside click event listening */
disableOnClickOutside(): void;
}
// Static method on the HOC wrapper component class
interface WrapperClass {
/** Returns the original wrapped component class */
getClass(): React.ComponentType;
}Usage - Props methods:
class Container extends Component {
handleToggle = () => {
if (this.dropdownRef) {
// Toggle outside click detection
if (this.outsideClickEnabled) {
this.dropdownRef.disableOnClickOutside();
} else {
this.dropdownRef.enableOnClickOutside();
}
this.outsideClickEnabled = !this.outsideClickEnabled;
}
};
render() {
return (
<div>
<button onClick={this.handleToggle}>Toggle Detection</button>
<WrappedDropdown
ref={ref => this.dropdownRef = ref}
disableOnClickOutside={true}
/>
</div>
);
}
}Usage - Access wrapped component:
class Container extends Component {
callWrappedMethod = () => {
if (this.wrapperRef) {
// Get reference to the actual wrapped component
const wrappedComponent = this.wrapperRef.getInstance();
// Call a method on the wrapped component
wrappedComponent.customMethod();
}
};
render() {
return (
<div>
<button onClick={this.callWrappedMethod}>Call Wrapped Method</button>
<WrappedComponent
ref={ref => this.wrapperRef = ref}
/>
</div>
);
}
}Controls whether outside click detection is initially disabled.
// From EnhancedProps interface above
disableOnClickOutside?: boolean; // default: falseUsage:
// Component starts with outside click detection disabled
<WrappedComponent disableOnClickOutside={true} />The wrapped component must implement a handler method for outside click events.
// Required method on wrapped component (unless provided via config or props)
interface RequiredHandler {
/** Handler method called when outside click is detected */
handleClickOutside(event: Event): void;
}
// Alternative: handler can be provided via props
interface PropsHandler {
/** Handler function passed as prop */
handleClickOutside?: (event: Event) => void;
}
// Alternative: handler can be configured via config object
interface ConfigHandler {
/** Function that returns the click handler for the component instance */
handleClickOutside: (instance: any) => (event: Event) => void;
}Handler priority (first available method is used):
handleClickOutside function resulthandleClickOutside functionhandleClickOutsideIf none are found, the HOC throws an error:
// Error thrown when no handler is found
throw new Error(
`WrappedComponent: ${componentName} lacks a handleClickOutside(event) function for processing outside click events.`
);The HOC will throw errors in these situations:
handleClickOutside method and no config handler is providedhandleClickOutside function doesn't return a function// Example error handling
try {
const WrappedComponent = onClickOutside(MyComponent);
} catch (error) {
console.error("HOC setup failed:", error.message);
}classList property support (all modern browsers)For IE11 SVG support, add this polyfill:
if (!("classList" in SVGElement.prototype)) {
Object.defineProperty(SVGElement.prototype, "classList", {
get() {
return {
contains: className => {
return this.className.baseVal.split(" ").indexOf(className) !== -1;
}
};
}
});
}