937

I'm looking for a way to detect if a click event happened outside of a component, as described in this article. jQuery closest() is used to see if the target from a click event has the dom element as one of its parents. If there is a match the click event belongs to one of the children and is thus not considered to be outside of the component.

So in my component, I want to attach a click handler to the window. When the handler fires I need to compare the target with the dom children of my component.

The click event contains properties like "path" which seems to hold the dom path that the event has traveled. I'm not sure what to compare or how to best traverse it, and I'm thinking someone must have already put that in a clever utility function... No?

Yilmaz
50.9k19 gold badges225 silver badges277 bronze badges
asked Sep 13, 2015 at 18:34
8
  • Could you attach the click handler to the parent rather than the window? Commented Sep 13, 2015 at 18:47
  • If you attach a click handler to the parent you know when that element or one of their children is clicked, but I need to detect all other places that are clicked, so the handler needs to be attached to the window. Commented Sep 13, 2015 at 18:50
  • I looked at the article after the previous response. How about setting a clickState in the top component and passing click actions from the kids. Then you would check the props in the kids to manage open close state. Commented Sep 13, 2015 at 19:01
  • The top component would be my app. But the listening component is several levels deep and has no strict position in the dom. I can't possibly add click handlers to all components in my app just because one of them is interested to know if you clicked somewhere outside of it. Other components should not be aware of this logic because that would create terrible dependencies and boilerplate code. Commented Sep 13, 2015 at 19:14
  • 22
    I would like to recommend you a very nice lib. created by AirBnb: github.com/airbnb/react-outside-click-handler Commented Apr 5, 2019 at 13:35

57 Answers 57

1
2
1598

The following solution uses ES6 and follows best practices for binding as well as setting the ref through a method.

To see it in action:

Hooks Implementation:

import React, { useRef, useEffect } from "react";
/**
 * Hook that alerts clicks outside of the passed ref
 */
function useOutsideAlerter(ref) {
 useEffect(() => {
 /**
 * Alert if clicked on outside of element
 */
 function handleClickOutside(event) {
 if (ref.current && !ref.current.contains(event.target)) {
 alert("You clicked outside of me!");
 }
 }
 // Bind the event listener
 document.addEventListener("mousedown", handleClickOutside);
 return () => {
 // Unbind the event listener on clean up
 document.removeEventListener("mousedown", handleClickOutside);
 };
 }, [ref]);
}
/**
 * Component that alerts if you click outside of it
 */
export default function OutsideAlerter(props) {
 const wrapperRef = useRef(null);
 useOutsideAlerter(wrapperRef);
 return <div ref={wrapperRef}>{props.children}</div>;
}

Class Implementation:

After 16.3

import React, { Component } from "react";
/**
 * Component that alerts if you click outside of it
 */
export default class OutsideAlerter extends Component {
 constructor(props) {
 super(props);
 this.wrapperRef = React.createRef();
 this.handleClickOutside = this.handleClickOutside.bind(this);
 }
 componentDidMount() {
 document.addEventListener("mousedown", this.handleClickOutside);
 }
 componentWillUnmount() {
 document.removeEventListener("mousedown", this.handleClickOutside);
 }
 /**
 * Alert if clicked on outside of element
 */
 handleClickOutside(event) {
 if (this.wrapperRef && !this.wrapperRef.current.contains(event.target)) {
 alert("You clicked outside of me!");
 }
 }
 render() {
 return <div ref={this.wrapperRef}>{this.props.children}</div>;
 }
}

Before 16.3

import React, { Component } from "react";
/**
 * Component that alerts if you click outside of it
 */
export default class OutsideAlerter extends Component {
 constructor(props) {
 super(props);
 this.setWrapperRef = this.setWrapperRef.bind(this);
 this.handleClickOutside = this.handleClickOutside.bind(this);
 }
 componentDidMount() {
 document.addEventListener("mousedown", this.handleClickOutside);
 }
 componentWillUnmount() {
 document.removeEventListener("mousedown", this.handleClickOutside);
 }
 /**
 * Set the wrapper ref
 */
 setWrapperRef(node) {
 this.wrapperRef = node;
 }
 /**
 * Alert if clicked on outside of element
 */
 handleClickOutside(event) {
 if (this.wrapperRef && !this.wrapperRef.contains(event.target)) {
 alert("You clicked outside of me!");
 }
 }
 render() {
 return <div ref={this.setWrapperRef}>{this.props.children}</div>;
 }
}
answered Feb 14, 2017 at 19:51
Sign up to request clarification or add additional context in comments.

63 Comments

How can you use React's synthetic events, instead of the document.addEventListener here?
@polkovnikov.ph 1. context is only necessary as an argument in constructor if it is used. It is not being used in this case. The reason the react team recommends to have props as an argument in the constructor is because use of this.props before calling super(props) will be undefined and can lead to errors. context is still available on inner nodes, that is the whole purpose of context. This way you don't have to pass it down from component to component like you do with props. 2. This is just a stylistic preference, and does not warrant debating over in this case.
I get the following error: "this.wrapperRef.contains is not a function"
@Bogdan , I was getting the same error when using a styled-component. Consider using a <div> on the top level
Thanks for this. It worked for some of my cases, but not for a popup that was display: absolute - it would always fire handleClickOutside when you click anywhere, including inside. Ended up switching to react-outside-click-handler, which seems to cover this case somehow
|
296

I was stuck on the same issue. I am a bit late to the party here, but for me this is a really good solution. Hopefully it will be of help to someone else. You need to import findDOMNode from react-dom

import ReactDOM from 'react-dom';
// ... ✂
componentDidMount() {
 document.addEventListener('click', this.handleClickOutside, true);
}
componentWillUnmount() {
 document.removeEventListener('click', this.handleClickOutside, true);
}
handleClickOutside = event => {
 const domNode = ReactDOM.findDOMNode(this);
 if (!domNode || !domNode.contains(event.target)) {
 this.setState({
 visible: false
 });
 }
}

React Hooks Approach (16.8 +)

You can create a reusable hook called useComponentVisible.

import { useState, useEffect, useRef } from 'react';
export default function useComponentVisible(initialIsVisible) {
 const [isComponentVisible, setIsComponentVisible] = useState(initialIsVisible);
 const ref = useRef(null);
 const handleClickOutside = (event) => {
 if (ref.current && !ref.current.contains(event.target)) {
 setIsComponentVisible(false);
 }
 };
 useEffect(() => {
 document.addEventListener('click', handleClickOutside, true);
 return () => {
 document.removeEventListener('click', handleClickOutside, true);
 };
 }, []);
 return { ref, isComponentVisible, setIsComponentVisible };
}

Then in the component you wish to add the functionality to do the following:

const DropDown = () => {
 const { ref, isComponentVisible } = useComponentVisible(true);
 return (
 <div ref={ref}>
 {isComponentVisible && (<p>Dropdown Component</p>)}
 </div>
 );
 
}

Find a codesandbox example here.

answered Jul 26, 2017 at 9:56

15 Comments

@LeeHanKyeol Not entirely - this answer invokes the event handlers during the capture phase of event handling, whereas the answer linked to invokes the event handlers during the bubble phase.
This should be the accepted answer. Worked perfectly for dropdown menus with absolute positioning.
ReactDOM.findDOMNode and is deprecated, should use ref callbacks: github.com/yannickcr/eslint-plugin-react/issues/…
This worked for me although I had to hack your component to reset visible back to 'true' after 200ms (otherwise my menu never shows again). Thank you!
this is the typing setup I've got with TS which seems to work: const ref = useRef<HTMLDivElement>(null); const handleClickOutside = (event: MouseEvent) => { const target = event.target as HTMLElement; if (ref.current && !ref.current.contains(target)) { setIsComponentVisible(false); }};
|
256

2021 Update:

It has bee a while since I added this response, and since it still seems to garner some interest, I thought I would update it to a more current React version. On 2021, this is how I would write this component:

import React, { useState } from "react";
import "./DropDown.css";
export function DropDown({ options, callback }) {
 const [selected, setSelected] = useState("");
 const [expanded, setExpanded] = useState(false);
 function expand() {
 setExpanded(true);
 }
 function close() {
 setExpanded(false);
 }
 function select(event) {
 const value = event.target.textContent;
 callback(value);
 close();
 setSelected(value);
 }
 return (
 <div className="dropdown" tabIndex={0} onFocus={expand} onBlur={close} >
 <div>{selected}</div>
 {expanded ? (
 <div className={"dropdown-options-list"}>
 {options.map((O) => (
 <div className={"dropdown-option"} onClick={select}>
 {O}
 </div>
 ))}
 </div>
 ) : null}
 </div>
 );
}

Original Answer (2016):

Here is the solution that best worked for me without attaching events to the container:

Certain HTML elements can have what is known as "focus", for example input elements. Those elements will also respond to the blur event, when they lose that focus.

To give any element the capacity to have focus, just make sure its tabindex attribute is set to anything other than -1. In regular HTML that would be by setting the tabindex attribute, but in React you have to use tabIndex (note the capital I).

You can also do it via JavaScript with element.setAttribute('tabindex',0)

This is what I was using it for, to make a custom DropDown menu.

var DropDownMenu = React.createClass({
 getInitialState: function(){
 return {
 expanded: false
 }
 },
 expand: function(){
 this.setState({expanded: true});
 },
 collapse: function(){
 this.setState({expanded: false});
 },
 render: function(){
 if(this.state.expanded){
 var dropdown = ...; //the dropdown content
 } else {
 var dropdown = undefined;
 }
 
 return (
 <div className="dropDownMenu" tabIndex="0" onBlur={ this.collapse } >
 <div className="currentValue" onClick={this.expand}>
 {this.props.displayValue}
 </div>
 {dropdown}
 </div>
 );
 }
});
answered May 27, 2016 at 20:13

17 Comments

Wouldn't this create problems with accessibility? Since you make an extra element focusable just to detect when it's going in/out focus, but the interaction should be on the dropdown values no?
I have this "working" to an extent, its a cool little hack. My issue is that my drop down content is display:absolute so that the drop down won't affect the parent div's size. This means when I click an item in the dropdown, the onblur fires.
I had issues with this approach, any element in the dropdown content doesn't fire events such as onClick.
You might face some other issues if the dropdown content contains some focusable elements like form inputs for example. They'll steal your focus, onBlur will be triggered on your dropdown container and expanded will be set to false.
For clicking on elements inside your target, see this answer: stackoverflow.com/a/44378829/642287
|
121

Hook implementation based on Tanner Linsley's excellent talk at JSConf Hawaii 2020:

useOuterClick API

const Client = () => {
 const innerRef = useOuterClick(ev => {/*event handler code on outer click*/});
 return <div ref={innerRef}> Inside </div> 
};

Implementation

function useOuterClick(callback) {
 const callbackRef = useRef(); // initialize mutable ref, which stores callback
 const innerRef = useRef(); // returned to client, who marks "border" element
 // update cb on each render, so second useEffect has access to current value 
 useEffect(() => { callbackRef.current = callback; });
 
 useEffect(() => {
 document.addEventListener("click", handleClick);
 return () => document.removeEventListener("click", handleClick);
 function handleClick(e) {
 if (innerRef.current && callbackRef.current && 
 !innerRef.current.contains(e.target)
 ) callbackRef.current(e);
 }
 }, []); // no dependencies -> stable click listener
 
 return innerRef; // convenience for client (doesn't need to init ref himself) 
}

Here is a working example:

/*
 Custom Hook
*/
function useOuterClick(callback) {
 const innerRef = useRef();
 const callbackRef = useRef();
 // set current callback in ref, before second useEffect uses it
 useEffect(() => { // useEffect wrapper to be safe for concurrent mode
 callbackRef.current = callback;
 });
 useEffect(() => {
 document.addEventListener("click", handleClick);
 return () => document.removeEventListener("click", handleClick);
 // read most recent callback and innerRef dom node from refs
 function handleClick(e) {
 if (
 innerRef.current && 
 callbackRef.current &&
 !innerRef.current.contains(e.target)
 ) {
 callbackRef.current(e);
 }
 }
 }, []); // no need for callback + innerRef dep
 
 return innerRef; // return ref; client can omit `useRef`
}
/*
 Usage 
*/
const Client = () => {
 const [counter, setCounter] = useState(0);
 const innerRef = useOuterClick(e => {
 // counter state is up-to-date, when handler is called
 alert(`Clicked outside! Increment counter to ${counter + 1}`);
 setCounter(c => c + 1);
 });
 return (
 <div>
 <p>Click outside!</p>
 <div id="container" ref={innerRef}>
 Inside, counter: {counter}
 </div>
 </div>
 );
};
ReactDOM.render(<Client />, document.getElementById("root"));
#container { border: 1px solid red; padding: 20px; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js" integrity="sha256-Ef0vObdWpkMAnxp39TYSLVS/vVUokDE8CDFnx7tjY6U=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js" integrity="sha256-p2yuFdE8hNZsQ31Qk+s8N+Me2fL5cc6NKXOC0U9uGww=" crossorigin="anonymous"></script>
<script> var {useRef, useEffect, useCallback, useState} = React</script>
<div id="root"></div>

Key points

  • useOuterClick makes use of mutable refs to provide lean Client API
  • stable click listener for lifetime of containing component ([] deps)
  • Client can set callback without needing to memoize it by useCallback
  • callback body has access to the most recent props and state - no stale closure values

(Side note for iOS)

iOS in general treats only certain elements as clickable. To make outer clicks work, choose a different click listener than document - nothing upwards including body. E.g. add a listener on the React root div and expand its height, like height: 100vh, to catch all outside clicks. Source: quirksmode.org

answered Jan 21, 2019 at 15:15

11 Comments

No errors but doesnt work on chrome, iphone 7+. It doesnt detect the taps outside. In chrome dev tools on mobile it works but in a real ios device its not working.
@Omar seems to be a very specific IOS oddity. Look here: quirksmode.org/blog/archives/2014/02/mouse_event_bub.html ,stackoverflow.com/questions/18524177/… , gravitydept.com/blog/js-click-event-bubbling-on-ios. The simplest workaround I can imagine is this: In the codesandbox sample, set an empty click handler for the root div like this: <div onClick={() => {}}> ... </div>, so that IOS registers all clicks from the outside. It works for me on Iphone 6+. Does that solve your problem?
i will need to test. but thanks for the solution. Doesnt it seem clunky though?
@ford04 thank your for digging that out, i stumbled upon another thread that was suggesting the following trick. @media (hover: none) and (pointer: coarse) { body { cursor:pointer } } Adding this in my global styles, seems it fixed the problem.
@ford04 yeah, btw according to this thread there is an even more specific media query @supports (-webkit-overflow-scrolling: touch) that targets only IOS even though i don't know more how much! I just tested it and it works.
|
118

After trying many methods here, I decided to use github.com/Pomax/react-onclickoutside because of how complete it is.

I installed the module via npm and imported it into my component:

import onClickOutside from 'react-onclickoutside'

Then, in my component class I defined the handleClickOutside method:

handleClickOutside = () => {
 console.log('onClickOutside() method called')
}

And when exporting my component I wrapped it in onClickOutside():

export default onClickOutside(NameOfComponent)

That's it.

answered Jan 11, 2017 at 1:51

6 Comments

Are there any concrete advantages to this over the tabIndex/onBlur approach proposed by Pablo? How does implementation work, and how does its behaviour differ from the onBlur approach?
Its better to user a dedicated component then using a tabIndex hack. Upvoting it!
@MarkAmery - I put a comment on the tabIndex/onBlur approach. It doesn't work when the dropdown is position:absolute, such as a menu hovering over other content.
Another advantage of using this over tabindex is that the tabindex solution also fire a blur event if you focus on a child element
Additionally, onBlur doesn't fire on iOS when clicking/tapping away from the focused element. Tapping away only removes hover state, while remaining focused.
|
66

[Update] Solution with React ^16.8 using Hooks

CodeSandbox

import React, { useEffect, useRef, useState } from 'react';
const SampleComponent = () => {
 const [clickedOutside, setClickedOutside] = useState(false);
 const myRef = useRef();
 const handleClickOutside = e => {
 if (!myRef.current.contains(e.target)) {
 setClickedOutside(true);
 }
 };
 const handleClickInside = () => setClickedOutside(false);
 useEffect(() => {
 document.addEventListener('mousedown', handleClickOutside);
 return () => document.removeEventListener('mousedown', handleClickOutside);
 });
 return (
 <button ref={myRef} onClick={handleClickInside}>
 {clickedOutside ? 'Bye!' : 'Hello!'}
 </button>
 );
};
export default SampleComponent;

Solution with React ^16.3:

CodeSandbox

import React, { Component } from "react";
class SampleComponent extends Component {
 state = {
 clickedOutside: false
 };
 componentDidMount() {
 document.addEventListener("mousedown", this.handleClickOutside);
 }
 componentWillUnmount() {
 document.removeEventListener("mousedown", this.handleClickOutside);
 }
 myRef = React.createRef();
 handleClickOutside = e => {
 if (!this.myRef.current.contains(e.target)) {
 this.setState({ clickedOutside: true });
 }
 };
 handleClickInside = () => this.setState({ clickedOutside: false });
 render() {
 return (
 <button ref={this.myRef} onClick={this.handleClickInside}>
 {this.state.clickedOutside ? "Bye!" : "Hello!"}
 </button>
 );
 }
}
export default SampleComponent;
answered May 28, 2018 at 3:32

4 Comments

This is the go-to solution now. If you're getting the error .contains is not a function, it may be because you're passing the ref prop to a custom component rather than a real DOM element like a <div>
For those trying to pass ref prop to a custom component, you may want to have a look at React.forwardRef
when you click on or hold scrollbar, it affects too. How to make it detect click outside but not scrollbar?
The .contains error (in this case 'contains' does not exist on type 'never') can also appear if you don't type useRef(), e.g.: const ref = useRef<HTMLDivElement>(null).
64

None of the other answers here worked for me. I was trying to hide a popup on blur, but since the contents were absolutely positioned, the onBlur was firing even on the click of inner contents too.

Here is an approach that did work for me:

// Inside the component:
onBlur(event) {
 // currentTarget refers to this component.
 // relatedTarget refers to the element where the user clicked (or focused) which
 // triggered this event.
 // So in effect, this condition checks if the user clicked outside the component.
 if (!event.currentTarget.contains(event.relatedTarget)) {
 // do your thing.
 }
},

Hope this helps.

answered Jun 5, 2017 at 22:45

5 Comments

Very good! Thanks. But in my case it was a problem to catch "current place" with position absolute for the div, so I use "OnMouseLeave" for div with input and drop down calendar to just disable all div when mouse leave the div.
I like this better than those methods attaching to document. Thanks.
For this to work you need to make sure that the clicked element (relatedTarget) is focusable. See stackoverflow.com/questions/42764494/…
Should honestly be the accepted answer, cleanest solution ITT
I also came across this solution and it is even documented in the official react docs: legacy.reactjs.org/docs/events.html. I like it because it seems to be more in line with react and doesn't involve adding event listeners manually.
50

The Ez way... (UPDATED 2023)

  • Create a hook: useOutsideClick.ts
export function useOutsideClick(ref: any, onClickOut: () => void, deps = []){
 useEffect(() => {
 const onClick = ({target}: any) => !ref?.contains(target) && onClickOut?.()
 document.addEventListener("click", onClick);
 return () => document.removeEventListener("click", onClick);
 }, deps);
}
  • Add componentRef to your component and call useOutsideClick
export function Example(){
 const ref: any = useRef();
 useOutsideClick(ref.current, () => {
 // do something here
 });
 return ( 
 <div ref={ref}> My Component </div>
 )
}
answered Jan 21, 2021 at 5:15

1 Comment

With this I dont need any ane react package. Thanks bro.
44

I found a solution thanks to Ben Alpert on discuss.reactjs.org. The suggested approach attaches a handler to the document but that turned out to be problematic. Clicking on one of the components in my tree resulted in a rerender which removed the clicked element on update. Because the rerender from React happens before the document body handler is called, the element was not detected as "inside" the tree.

The solution to this was to add the handler on the application root element.

main:

window.__myapp_container = document.getElementById('app')
React.render(<App/>, window.__myapp_container)

component:

import { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';
export default class ClickListener extends Component {
 static propTypes = {
 children: PropTypes.node.isRequired,
 onClickOutside: PropTypes.func.isRequired
 }
 componentDidMount () {
 window.__myapp_container.addEventListener('click', this.handleDocumentClick)
 }
 componentWillUnmount () {
 window.__myapp_container.removeEventListener('click', this.handleDocumentClick)
 }
 /* using fat arrow to bind to instance */
 handleDocumentClick = (evt) => {
 const area = ReactDOM.findDOMNode(this.refs.area);
 if (!area.contains(evt.target)) {
 this.props.onClickOutside(evt)
 }
 }
 render () {
 return (
 <div ref='area'>
 {this.props.children}
 </div>
 )
 }
}
Bhargav Ponnapalli
9,4427 gold badges38 silver badges45 bronze badges
answered Sep 26, 2015 at 8:36

5 Comments

This does not work for me any more with React 0.14.7 - maybe React changed something, or maybe I made an error when adapting the code to all the changes to React. I'm instead using github.com/Pomax/react-onclickoutside which works like a charm.
Hmm. I don't see any reason why this should work. Why should a handler on a the app's root DOM node be guaranteed to fire before a rerender triggered by another handler if one on the document isn't?
A pure UX point: mousedown would probably be a better handler than click here. In most applications, the close-menu-by-clicking-outside behaviour happens the moment that you mouse down, not when you release. Try it, for instance, with Stack Overflow's flag or share dialogues or with one of the dropdowns from your browser's top menu bar.
ReactDOM.findDOMNode and string ref are deprecated, should use ref callbacks: github.com/yannickcr/eslint-plugin-react/issues/…
this is the most simple solution and works perfectly for me even when attaching to the document
38

MUI has a small component to solve this problem: https://mui.com/base/react-click-away-listener/ that you can cherry-pick it. It weights below 1 kB gzipped, it supports mobile, IE 11, and portals.

answered Jan 25, 2020 at 21:30

Comments

19

Alternatively:

const onClickOutsideListener = () => {
 alert("click outside")
 document.removeEventListener("click", onClickOutsideListener)
 }
...
return (
 <div
 onMouseLeave={() => {
 document.addEventListener("click", onClickOutsideListener)
 }}
 >
 ...
 </div>
answered Sep 21, 2020 at 10:16

1 Comment

Well, simplicity is the goal... isn't it! 👌
15

with typescript

function Tooltip(): ReactElement {
 const [show, setShow] = useState(false);
 const ref = useRef<HTMLDivElement>(null);
 useEffect(() => {
 function handleClickOutside(event: MouseEvent): void {
 if (ref.current && !ref.current.contains(event.target as Node)) {
 setShow(false);
 }
 }
 // Bind the event listener
 document.addEventListener('mousedown', handleClickOutside);
 return () => {
 // Unbind the event listener on clean up
 document.removeEventListener('mousedown', handleClickOutside);
 };
 });
 return (
 <div ref={ref}></div>
 ) 
 }

answered Apr 14, 2022 at 7:45

Comments

10
import { useClickAway } from "react-use";
useClickAway(ref, () => console.log('OUTSIDE CLICKED'));
Borja
3,62125 gold badges51 silver badges51 bronze badges
answered May 10, 2020 at 20:59

4 Comments

Use this recommended way to import: import useClickAway from 'react-use/lib/useClickAway'; to avoid importing the whole library
@Vad no need, bundlers like Webpack performs tree shaking
@Melounek no they don't
9

For those who need absolute positioning, a simple option I opted for is to add a wrapper component that is styled to cover the whole page with a transparent background. Then you can add an onClick on this element to close your inside component.

<div style={{
 position: 'fixed',
 top: '0', right: '0', bottom: '0', left: '0',
 zIndex: '1000',
 }} onClick={() => handleOutsideClick()} >
 <Content style={{position: 'absolute'}}/>
</div>

As it is right now if you add a click handler on content, the event will also be propagated to the upper div and therefore trigger the handlerOutsideClick. If this is not your desired behavior, simply stop the event progation on your handler.

<Content style={{position: 'absolute'}} onClick={e => {
 e.stopPropagation();
 desiredFunctionCall();
 }}/>

`

answered Feb 14, 2017 at 19:33

5 Comments

The issue with this approach is that you can't have any clicks on the Content - the div will receive the click instead.
Since the content is in the div if you do not stop the event propagation both will receive the click. This is often a desired behavior but if you do not want the click to be propagated to the div, simply stop the eventPropagation on your content onClick handler. I have update my answer to show how.
This will not allow you to interact with other elements on your page though, since they will be covered by the wrapper div
This solution should be more upvoted. One of its main advantage, beyond being easy to implement, is that it disable interaction with other element. This is the default behavior of native alert & dropdown and thus should be used for all custom implementation of modal & dropdown.
To be able to interact with any of desired components while wrapper is active just have higher z-index for those components that you want to interact with.
9
import { RefObject, useEffect } from 'react';
const useClickOutside = <T extends HTMLElement>(ref: RefObject<T>, fn: () => void) => {
 useEffect(() => {
 const element = ref?.current;
 function handleClickOutside(event: Event) {
 if (element && !element.contains(event.target as Node | null)) {
 fn();
 }
 }
 document.addEventListener('mousedown', handleClickOutside);
 return () => {
 document.removeEventListener('mousedown', handleClickOutside);
 };
 }, [ref, fn]);
};
export default useClickOutside;
answered Oct 20, 2022 at 13:31

1 Comment

Your useEffect is missing fn as a dependency, thus if it changes for some reason, the useEffect will not update and you will be referencing a stale version of it.
7

Here is my approach (demo - https://jsfiddle.net/agymay93/4/):

I've created special component called WatchClickOutside and it can be used like (I assume JSX syntax):

<WatchClickOutside onClickOutside={this.handleClose}>
 <SomeDropdownEtc>
</WatchClickOutside>

Here is code of WatchClickOutside component:

import React, { Component } from 'react';
export default class WatchClickOutside extends Component {
 constructor(props) {
 super(props);
 this.handleClick = this.handleClick.bind(this);
 }
 componentWillMount() {
 document.body.addEventListener('click', this.handleClick);
 }
 componentWillUnmount() {
 // remember to remove all events to avoid memory leaks
 document.body.removeEventListener('click', this.handleClick);
 }
 handleClick(event) {
 const {container} = this.refs; // get container that we'll wait to be clicked outside
 const {onClickOutside} = this.props; // get click outside callback
 const {target} = event; // get direct click event target
 // if there is no proper callback - no point of checking
 if (typeof onClickOutside !== 'function') {
 return;
 }
 // if target is container - container was not clicked outside
 // if container contains clicked target - click was not outside of it
 if (target !== container && !container.contains(target)) {
 onClickOutside(event); // clicked outside - fire callback
 }
 }
 render() {
 return (
 <div ref="container">
 {this.props.children}
 </div>
 );
 }
}
answered Jan 14, 2017 at 17:18

3 Comments

Great solution - for my use, I changed to listen to document instead of body in case of pages with short content, and changed ref from string to dom reference: jsfiddle.net/agymay93/9
and used span instead of div as it's less likely to change layout, and added demo of handling clicks outside of body: jsfiddle.net/agymay93/10
String ref is deprecated, should use ref callbacks: reactjs.org/docs/refs-and-the-dom.html
7

Typescript + simplified version of @ford04's proposal:

useOuterClick API

const Client = () => {
 const ref = useOuterClick<HTMLDivElement>(e => { /* Custom-event-handler */ });
 return <div ref={ref}> Inside </div> 
};

Implementation

export default function useOuterClick<T extends HTMLElement>(callback: Function) {
 const callbackRef = useRef<Function>(); // initialize mutable ref, which stores callback
 const innerRef = useRef<T>(null); // returned to client, who marks "border" element
 // update cb on each render, so second useEffect has access to current value
 useEffect(() => { callbackRef.current = callback; });
 useEffect(() => {
 document.addEventListener("click", _onClick);
 return () => document.removeEventListener("click", _onClick);
 function _onClick(e: any): void {
 const clickedOutside = !(innerRef.current?.contains(e.target));
 if (clickedOutside)
 callbackRef.current?.(e);
 }
 }, []); // no dependencies -> stable click listener
 return innerRef; // convenience for client (doesn't need to init ref himself)
}
answered Apr 3, 2022 at 13:21

1 Comment

This is my favorite approach. I like that it reduces duplicated code and it also enforces putting all the logic on the trigger
7

Simply with ClickAwayListener from mui (material-ui):

<ClickAwayListener onClickAway={handleClickAway}>
 {children}
<ClickAwayListener >

for more info you can check:https://mui.com/base/react-click-away-listener/

answered Apr 11, 2022 at 12:20

Comments

6

This already has many answers but they don't address e.stopPropagation() and preventing clicking on react links outside of the element you wish to close.

Due to the fact that React has it's own artificial event handler you aren't able to use document as the base for event listeners. You need to e.stopPropagation() before this as React uses document itself. If you use for example document.querySelector('body') instead. You are able to prevent the click from the React link. Following is an example of how I implement click outside and close.
This uses ES6 and React 16.3.

import React, { Component } from 'react';
class App extends Component {
 constructor(props) {
 super(props);
 this.state = {
 isOpen: false,
 };
 this.insideContainer = React.createRef();
 }
 componentWillMount() {
 document.querySelector('body').addEventListener("click", this.handleClick, false);
 }
 componentWillUnmount() {
 document.querySelector('body').removeEventListener("click", this.handleClick, false);
 }
 handleClick(e) {
 /* Check that we've clicked outside of the container and that it is open */
 if (!this.insideContainer.current.contains(e.target) && this.state.isOpen === true) {
 e.preventDefault();
 e.stopPropagation();
 this.setState({
 isOpen: false,
 })
 }
 };
 togggleOpenHandler(e) {
 e.preventDefault();
 this.setState({
 isOpen: !this.state.isOpen,
 })
 }
 render(){
 return(
 <div>
 <span ref={this.insideContainer}>
 <a href="#open-container" onClick={(e) => this.togggleOpenHandler(e)}>Open me</a>
 </span>
 <a href="/" onClick({/* clickHandler */})>
 Will not trigger a click when inside is open.
 </a>
 </div>
 );
 }
}
export default App;
answered Jul 11, 2018 at 17:57

Comments

6

I did this partly by following this and by following the React official docs on handling refs which requires react ^16.3. This is the only thing that worked for me after trying some of the other suggestions here...

class App extends Component {
 constructor(props) {
 super(props);
 this.inputRef = React.createRef();
 }
 componentWillMount() {
 document.addEventListener("mousedown", this.handleClick, false);
 }
 componentWillUnmount() {
 document.removeEventListener("mousedown", this.handleClick, false);
 }
 handleClick = e => {
 /*Validating click is made inside a component*/
 if ( this.inputRef.current === e.target ) {
 return;
 }
 this.handleclickOutside();
 };
 handleClickOutside(){
 /*code to handle what to do when clicked outside*/
 }
 render(){
 return(
 <div>
 <span ref={this.inputRef} />
 </div>
 )
 }
}
Yogi
1,76217 silver badges22 bronze badges
answered Apr 24, 2018 at 11:00

1 Comment

Correct This this.handleclickOutside(); it should be this.handleClickOutside()
5

Typescript with Hooks

Note: I'm using React version 16.3, with React.createRef. For other versions use the ref callback.

Dropdown component:

interface DropdownProps {
 ...
};
export const Dropdown: React.FC<DropdownProps> () {
 const ref: React.RefObject<HTMLDivElement> = React.createRef();
 
 const handleClickOutside = (event: MouseEvent) => {
 if (ref && ref !== null) {
 const cur = ref.current;
 if (cur && !cur.contains(event.target as Node)) {
 // close all dropdowns
 }
 }
 }
 useEffect(() => {
 // Bind the event listener
 document.addEventListener("mousedown", handleClickOutside);
 return () => {
 // Unbind the event listener on clean up
 document.removeEventListener("mousedown", handleClickOutside);
 };
 });
 return (
 <div ref={ref}>
 ...
 </div>
 );
}
answered Jul 11, 2020 at 14:10

1 Comment

Came here for the TypeScript solution... is there no other way around the event.target as Node part?
4

To extend on the accepted answer made by Ben Bud, if you are using styled-components, passing refs that way will give you an error such as "this.wrapperRef.contains is not a function".

The suggested fix, in the comments, to wrap the styled component with a div and pass the ref there, works. Having said that, in their docs they already explain the reason for this and the proper use of refs within styled-components:

Passing a ref prop to a styled component will give you an instance of the StyledComponent wrapper, but not to the underlying DOM node. This is due to how refs work. It's not possible to call DOM methods, like focus, on our wrappers directly. To get a ref to the actual, wrapped DOM node, pass the callback to the innerRef prop instead.

Like so:

<StyledDiv innerRef={el => { this.el = el }} />

Then you can access it directly within the "handleClickOutside" function:

handleClickOutside = e => {
 if (this.el && !this.el.contains(e.target)) {
 console.log('clicked outside')
 }
}

This also applies for the "onBlur" approach:

componentDidMount(){
 this.el.focus()
}
blurHandler = () => {
 console.log('clicked outside')
}
render(){
 return(
 <StyledDiv
 onBlur={this.blurHandler}
 tabIndex="0"
 innerRef={el => { this.el = el }}
 />
 )
}
answered Jul 5, 2018 at 2:36

Comments

4

So I faced a similar problem but in my case the selected answer here wasn't working because I had a button for the dropdown which is, well, a part of the document. So clicking the button also triggered the handleClickOutside function. To stop that from triggering, I had to add a new ref to the button and this !menuBtnRef.current.contains(e.target) to the conditional. I'm leaving it here if someone is facing the same issue like me.

Here's how the component looks like now:


const Component = () => {
 const [isDropdownOpen, setIsDropdownOpen] = useState(false);
 const menuRef = useRef(null);
 const menuBtnRef = useRef(null);
 const handleDropdown = (e) => {
 setIsDropdownOpen(!isDropdownOpen);
 }
 const handleClickOutside = (e) => {
 if (menuRef.current && !menuRef.current.contains(e.target) && !menuBtnRef.current.contains(e.target)) {
 setIsDropdownOpen(false);
 }
 }
 useEffect(() => {
 document.addEventListener('mousedown', handleClickOutside, true);
 return () => {
 document.removeEventListener('mousedown', handleClickOutside, true);
 };
 }, []);
 return (
 <button ref={menuBtnRef} onClick={handleDropdown}></button>
 <div ref={menuRef} className={`${isDropdownOpen ? styles.dropdownMenuOpen : ''}`}>
 // ...dropdown items
 </div>
 )
}
answered Feb 4, 2022 at 15:56

Comments

4

This is my way of solving the problem

I return a boolean value from my custom hook, and when this value changes (true if the click was outside of the ref that I passed as an arg), this way i can catch this change with an useEffect hook, i hope it's clear for you.

Here's a live example: Live Example on codesandbox

import { useEffect, useRef, useState } from "react";
const useOutsideClick = (ref) => {
 const [outsieClick, setOutsideClick] = useState(null);
 useEffect(() => {
 const handleClickOutside = (e) => {
 if (!ref.current.contains(e.target)) {
 setOutsideClick(true);
 } else {
 setOutsideClick(false);
 }
 setOutsideClick(null);
 };
 document.addEventListener("mousedown", handleClickOutside);
 return () => {
 document.removeEventListener("mousedown", handleClickOutside);
 };
 }, [ref]);
 return outsieClick;
};
export const App = () => {
 const buttonRef = useRef(null);
 const buttonClickedOutside = useOutsideClick(buttonRef);
 useEffect(() => {
 // if the the click was outside of the button
 // do whatever you want
 if (buttonClickedOutside) {
 alert("hey you clicked outside of the button");
 }
 }, [buttonClickedOutside]);
 return (
 <div className="App">
 <button ref={buttonRef}>click outside me</button>
 </div>
 );
}
answered Mar 24, 2022 at 18:31

Comments

4

If you need typescript version:

import React, { useRef, useEffect } from "react";
interface Props {
 ref: React.MutableRefObject<any>;
}
export const useOutsideAlerter = ({ ref }: Props) => {
 useEffect(() => {
 const handleClickOutside = (event: MouseEvent) => {
 if (ref.current && !ref.current.contains(event.target as Node)) {
 //do what ever you want
 }
 };
 // Bind the event listener
 document.addEventListener("mousedown", handleClickOutside);
 return () => {
 // Unbind the event listener on clean up
 document.removeEventListener("mousedown", handleClickOutside);
 };
 }, [ref]);
};
export default useOutsideAlerter;

If you want to extend this to close a modal or hide something you can also do:

import React, { useRef, useEffect } from "react";
interface Props {
 ref: React.MutableRefObject<any>;
 setter: React.Dispatch<React.SetStateAction<boolean>>;
}
export const useOutsideAlerter = ({ ref, setter }: Props) => {
 useEffect(() => {
 const handleClickOutside = (event: MouseEvent) => {
 if (ref.current && !ref.current.contains(event.target as Node)) {
 setter(false);
 }
 };
 // Bind the event listener
 document.addEventListener("mousedown", handleClickOutside);
 return () => {
 // Unbind the event listener on clean up
 document.removeEventListener("mousedown", handleClickOutside);
 };
 }, [ref, setter]);
};
export default useOutsideAlerter;
answered Jan 25, 2023 at 17:24

Comments

4

There is an npm module which will make your life easier to handle the clicks outside a specific component. For Example: You make states as true and false. On dropdown a menu list your state is true and on clicking on close button your state converts to false and dropdown menu component gets disappear. But you want to close this drop down menu also clicking on outside of the drop down menu on the window. To deal with such scenario follow the below steps:

 npm i react-outside-click-handler

Now Import this module in your React File:

import OutsideClickHandler from 'react-outside-click-handler';

Now You have imported a component from this module. This component takes a component outside of which you want to detect a click event

function MyComponent() {
 return (
 <OutsideClickHandler
 onOutsideClick={() => {
 alert("You clicked outside of this component!!!");
 //Or any logic you want
 }} >
 <yourComponent />
 </OutsideClickHandler>
 );
}

Now Simply replace you Own component with . I hope this find you helpful :)

answered Jul 22, 2023 at 19:49

1 Comment

It didn't work for me with react-detect-click-outside but it worked with react-outside-click-handler
3

UseOnClickOutside Hook - React 16.8 +

Create a general useOnOutsideClick function

export const useOnOutsideClick = handleOutsideClick => {
 const innerBorderRef = useRef();
 const onClick = event => {
 if (
 innerBorderRef.current &&
 !innerBorderRef.current.contains(event.target)
 ) {
 handleOutsideClick();
 }
 };
 useMountEffect(() => {
 document.addEventListener("click", onClick, true);
 return () => {
 document.removeEventListener("click", onClick, true);
 };
 });
 return { innerBorderRef };
};
const useMountEffect = fun => useEffect(fun, []);

Then use the hook in any functional component.

const OutsideClickDemo = ({ currentMode, changeContactAppMode }) => {
 const [open, setOpen] = useState(false);
 const { innerBorderRef } = useOnOutsideClick(() => setOpen(false));
 return (
 <div>
 <button onClick={() => setOpen(true)}>open</button>
 {open && (
 <div ref={innerBorderRef}>
 <SomeChild/>
 </div>
 )}
 </div>
 );
};

Link to demo

Partially inspired by @pau1fitzgerald answer.

answered Jun 18, 2019 at 8:07

2 Comments

This is one of the many awesome hooks posted on useHooks.com - usehooks.com/useOnClickOutside
the best answer
3

I had a similar use case where I had to develop a custom dropdown menu. it should close automatically when the user clicks outside. here is the recent React Hooks implementation-

import { useEffect, useRef, useState } from "react";
export const App = () => {
 
 const ref = useRef();
 const [isMenuOpen, setIsMenuOpen] = useState(false);
 useEffect(() => {
 const checkIfClickedOutside = (e) => {
 // If the menu is open and the clicked target is not within the menu,
 // then close the menu
 if (isMenuOpen && ref.current && !ref.current.contains(e.target)) {
 setIsMenuOpen(false);
 }
 };
 document.addEventListener("mousedown", checkIfClickedOutside);
 return () => {
 // Cleanup the event listener
 document.removeEventListener("mousedown", checkIfClickedOutside);
 };
 }, [isMenuOpen]);
 return (
 <div className="wrapper" ref={ref}>
 <button
 className="button"
 onClick={() => setIsMenuOpen((oldState) => !oldState)}
 >
 Click Me
 </button>
 {isMenuOpen && (
 <ul className="list">
 <li className="list-item">dropdown option 1</li>
 <li className="list-item">dropdown option 2</li>
 <li className="list-item">dropdown option 3</li>
 <li className="list-item">dropdown option 4</li>
 </ul>
 )}
 </div>
 );
}

answered Apr 7, 2022 at 19:04

Comments

3

Since for me the !ref.current.contains(e.target) wasn't working because the DOM elements contained inside the ref were changing, I came up with a slightly different solution:

function useClickOutside<T extends HTMLElement>(
 element: T | null,
 onClickOutside: () => void,
) {
 useEffect(() => {
 function handleClickOutside(event: MouseEvent) {
 const xCoord = event.clientX;
 const yCoord = event.clientY;
 if (element) {
 const { right, x, bottom, y } = element.getBoundingClientRect();
 if (xCoord < right && xCoord > x && yCoord < bottom && yCoord > y) {
 return;
 }
 onClickOutside();
 }
 }
 document.addEventListener('click', handleClickOutside);
 return () => {
 document.removeEventListener('click', handleClickOutside);
 };
 }, [element, onClickOutside]);
answered Aug 23, 2022 at 12:22

1 Comment

I used the Hook solution, but also thanks to your solution, I used yCoord to check it was below an un-managed Header. Cheers.
3

Try this it works perfectly

import React, { useState, useRef, useEffect } from "react";
const Dropdown = () => {
 const [state, setState] = useState(true);
 const dropdownRef = useRef(null);
 const handleToggleDropdown = () => {
 if (state) setState(false);
 else setState(true);
 }
 const handleClickOutside = (event) => {
 if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
 setState(true);
 }
 }
 useEffect(() => {
 document.addEventListener('mousedown', handleClickOutside);
 return () => {
 document.removeEventListener('mousedown', handleClickOutside);
 }
 }, []);
 return (
 <React.Fragment>
 <div className="dropdown" ref={dropdownRef}>
 <button onClick={() => handleToggleDropdown()}>
 Options
 </button>
 <div className="dropdown-content" hidden={state}>
 </div>
 </div>
 </React.Fragment>
 );
}
export default Dropdown;
answered Apr 17, 2023 at 15:03

Comments

1
2

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.