React Click Outside (Away) Example
In this article, I will show you how to detect and handle when the user clicks outside a modal dialog box or popup.

In this article, I will show you how to detect and handle when the user clicks outside a modal dialog box or popup.
Improving Usability
It's easy to create a popup or dialog box triggered by a button click. It's also easy to let another click of the button close the popup. What's more difficult is figuring out when the user has clicked away from a popup, expecting it to close as they move on to another task.
With the steps below I'll show you how to create an example and add "click outside" (also known as "click away") functionality.
These steps were tested on a Mac.
Step 1. Create a new React app
In this step, I will have you create a React app. To keep things simple it will just be a React JavaScript app (as opposed to a TypeScript app).
- Open up a new terminal window
- Switch to the root folder for your projects (like cd projects)
- Run the following commands:
npx create-react-app react-click-away-demo
cd react-click-away-demo
- Then open the project in your favorite code editor
Step 2. Define the dialog flags
In this example, there are going to be popup dialog boxes. Their visibility will be controlled by a flag for each one. To define those flags do the following:
- Open src/App.js
- Add this line to the top of it:
import { useState } from 'react';
- Add the following to the top of the App function:
// Define dialog display flags
const [showDialogA, setShowDialogA] = useState(false);
const [showDialogB, setShowDialogB] = useState(false);
const [showDialogC, setShowDialogC] = useState(false);
This defines three values to track as many dialog box states, defaulting to false.
If the flag is true, that means show the dialog box and false means hide it.
Step 3. Add a function to clear all the dialog flags
When users click away, they expect all modal dialog boxes and popups to be closed. This can be done by simply turning all related flags false.
- Add this below the last block of code:
// Define function to clear all dialogs
const clearDialogs = () => {
setShowDialogA(false)
setShowDialogB(false)
setShowDialogC(false)
}
Calling the function will set all flags to false, closing any open dialog boxes.
Step 4. Define handlers for the dialog activation buttons
Each of the buttons will first clear all open dialog boxes (by calling the clearDialogs function, defined above). Then they will toggle the display flag for their related dialog box.
- Add this code after the previous block defined above:
// Define handler for clicking the first button
const handleClickAlpha = (event) => {
clearDialogs()
setShowDialogA(!showDialogA)
};
// Define handler for clicking the second button
const handleClickBeta = (event) => {
clearDialogs()
setShowDialogB(!showDialogB)
};
// Define handler for clicking the third button
const handleClickGamma = (event) => {
clearDialogs()
setShowDialogC(!showDialogC)
};
The three handlers will be wired to their respective buttons in a later step.
Step 5. Define dynamic styles
One way to control the visibility of a popup or dialog box is with a dynamic style that can be changed at runtime.
- Add the following below the code above:
// Define a style to control the display of the first dialog
let styleA = {
display: showDialogA ? 'block' : 'none',
}
// Define a style to control the display of the second dialog
let styleB = {
display: showDialogB ? 'block' : 'none',
}
// Define a style to control the display of the third dialog
let styleC = {
display: showDialogC ? 'block' : 'none',
}
If the flag for a dialog box is set to true, the display property will be set to block (display). Otherwise, it will be set to none (hide).
In the next step, I will show you how to assign each style to its respective dialog box.
Step 6. Define the buttons and dialog boxes
Replace everything in the return statement with the following:
return (
<div className="App">
<div className="pair">
<button className="item" onClick={handleClickAlpha}>A</button>
<div id="childA" className="dialog" style={styleA}></div>
</div>
<div className="pair">
<button className="item" onClick={handleClickBeta}>B</button>
<div id="childB" className="dialog" style={styleB}></div>
</div>
<div className="pair">
<button className="item" onClick={handleClickGamma}>C</button>
<div id="childC" className="dialog" style={styleC}></div>
</div>
</div>
);
This code above replaces the contents of the App div with three divs representing each button + dialog pair (A, B, and C).
The div with class "pair" holds two children:
- button - the button to click to toggle the dialog display state
- div - the div representing the dialog box to show and hide with the paired button click
The buttons
- The buttons map to an item class (defined later in an external .css file)
- They also map to their button onClick handlers that were defined previously
The dialog divs
- The dialog divs contain an id attribute which will be used to map a background color to make each dialog box unique
- They also contain a className attribute ("dialog") that will be defined in the next step in an external .css file
- Finally, they contain a style attribute that maps the dynamic style for each dialog box
The styles will be defined in the next step.
Step 7. Add more styles
- At the top of App.js import a new style sheet:
import './demo.css';
- Create a file called demo.css in the src folder and fill it with the following:
.pair {
padding: 20px;
width : 50px;
height: 100px;
border: 1px solid black;
background-color: lightgray;
margin: 5px;
display: inline-block;
}
.item {
font-size: 25px;
width: 50px;
height: 50px;
border-radius: 50%;
}
.item:hover {
background-color: lightpink;
transition: 0.7s;
}
.dialog {
width: 200px;
height: 200px;
border: 1px solid black;
position: relative;
top: 20px;
left: -75px;
}
#childA {
background-color: lightgreen;
}
#childB {
background-color: lightcyan;
}
#childC {
background-color: lightblue;
}
The most important thing to note is how the .dialog style defines the div as being relative to the previous div (the button). This is how you can position the dialog relative to its parent (the button). The properties that control this are position, top, and left:
.dialog {
/* ... */
position: relative;
top: 20px;
left: -75px;
}
Step 8. Run without the fix
The example so far should work. But I haven't shown you yet how to deal with the usability issues. To get a sense of that, run the current example as is.
- From the terminal window do the following:
npm start
- Browse to:
http://localhost:3000
In the browser click the various buttons. Notice the behavior. It works as expected – until you click outside of a dialog box. Instinctively you would expect the dialog box to just close. But it doesn't. It will only close if you click one of the buttons. To a user, this subtle but non-standard behavior can be a bit jarring.
Step 9. Update the import
To improve usability and add click-away capabilities we are going to need to use React's useEffect and useRef hooks. Update the import line at the top of App.js to include them, like this:
import { useState, useEffect, useRef } from 'react';
Step 10. Add the refs
At the top of the App function insert this code to define some refs:
// Define button + dialog reference pairs
const RefPair = () => ({ button: useRef(), dialog: useRef() })
const refA = RefPair();
const refB = RefPair();
const refC = RefPair();
For each dialog box, it creates a pair of refs. One for the button. One for the dialog box.
What is a ref?
A ref is a reference to a DOM node. When used below it will return a reference to the DOM node that it is associated with. That way you can do things like compare if the current node associated with the ref is the one clicked on by the mouse.
Step 11. Add useEffect
Below where you defined the clearDialogs function, insert this code:
// Define useEffect to handle click events
useEffect(() => {
const handleMouseDown = e => {
// requires node 14 or greater
let isOver = (rf) => rf.current?.contains(e.target);
let checkOutside = function (flag, rp) {
if (flag && !isOver(rp.button) && !isOver(rp.dialog)) {
clearDialogs();
}
}
// important to reference flags below for useEffect
// if a flag isn't referenced this won't work for that flag
checkOutside(showDialogA, refA);
checkOutside(showDialogB, refB);
checkOutside(showDialogC, refC);
}
document.addEventListener("mousedown", handleMouseDown);
return () => {
// Cleanup the event listener
document.removeEventListener("mousedown", handleMouseDown);
}
}, [
showDialogA,
refA,
showDialogB,
refB,
showDialogC,
refC,
])
The useEffect hook is described as allowing you to interact with external systems. But what does that mean? Think of it as an event monitor operating off to the side. In this example app, the external system it interacts with is the mouse (in other cases it could be a timer, fetching data, etc.)
The main thing is that it sets up a mousedown event handler. When the user clicks the mouse, the handler is called.
An arrow function is defined within the handler:
- isOver returns true if the mousedown event.target equals the node associated with the ref passed to it
Note that the isOver function uses optional chaining - which requires Node 14 or higher.
Next, a checkOutside function is also defined within the handler. It takes two arguments:
- flag - the display flag for the dialog box
- rp - an object containing the paired button and dialog refs
The function will call the clearDialogs function if:
- The flag is true - indicating that the associated dialog box is visible
- The mousedown event did not happen over the button
- The mousedown event did not happen over the dialog box
Step 12. Add refs to the controls
The final piece is to update the App button and dialogs divs with refs for each button and dialog below. For example, see new attributes such as ref={refA.button}
:
// Layout with refs
return (
<div className="App">
<div className="pair">
<button className="item" ref={refA.button} onClick={handleClickAlpha}>A</button>
<div id="childA" className="dialog" ref={refA.dialog} style={styleA}></div>
</div>
<div className="pair">
<button className="item" ref={refB.button} onClick={handleClickBeta}>B</button>
<div id="childB" className="dialog" ref={refB.dialog} style={styleB}></div>
</div>
<div className="pair">
<button className="item" ref={refC.button} onClick={handleClickGamma}>C</button>
<div id="childC" className="dialog" ref={refC.dialog} style={styleC}></div>
</div>
</div>
);
Step 13. Test usability
Run the app:
npm start
- Click the buttons to popup the dialog boxes
- Click outside the buttons and the dialog boxes to verify that they close
- Verify that you can still toggle the dialogs with the buttons
- Verify that you can click inside the dialog box and it won't accidentally close
Complete code
You can find the complete code here:
Conclusion
In this article, I showed you how to handle events outside a dialog box. By managing those events you can use them to provide "click-away" (aka "click-outside") functionality and a better user experience.