If you’re familiar with React, you are most likely already aware of the fact that React renders all the HTML elements under a single div tag, often given an ID of root.
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
This can be annoyingly restrictive if you are trying to render another element, such as a modal or a tooltip, outside the root element. In a bid to solve this problem, React portals were introduced.
The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
In version 16.0 of React, portals were introduced to solve the inability to render other elements outside the root node.
Here is an example from the React docs on how to make use of React portals:
const appRoot = document.getElementById('app-root');
const modalRoot = document.getElementById('modal-root');
class Modal extends React.Component {
constructor(props) {
super(props);
this.el = document.createElement('div');
}
componentDidMount() {
modalRoot.appendChild(this.el);
}
componentWillUnmount() {
modalRoot.removeChild(this.el);
}
render() {
return ReactDOM.createPortal(
this.props.children,
this.el
);
}
}
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {clicks: 0};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
// This will fire when the button in Child is clicked,
// updating Parent's state, even though button
// is not direct descendant in the DOM.
this.setState(state => ({
clicks: state.clicks + 1
}));
}
render() {
return (
<div onClick={this.handleClick}>
<p>Number of clicks: {this.state.clicks}</p>
<p>
Open up the browser DevTools
to observe that the button
is not a child of the div
with the onClick handler.
</p>
<Modal>
<Child />
</Modal>
</div>
);
}
}
function Child() {
// The click event on this button will bubble up to parent,
// because there is no 'onClick' attribute defined
return (
<div className="modal">
<button>Click</button>
</div>
);
}
ReactDOM.render(<Parent />, appRoot);
As you can see, the code is a little verbose and could be more readable and understandable if its length were reduced. Fast-forward to the introduction of Hooks in React version 16.8.
Hooks allow developers to reuse stateful logic without changing component hierarchy. They have changed the way React developers write code and compose state. In fact, many React libraries, such as react-redux and react-router, are moving — or have already moved — towards a more Hooks-based API.
Many new Hooks-based libraries are also being released to provide some abstraction over certain services or code. One such library is react-cool-portal. In this article, we will explore react-cool-portal, and by the end, we will have built an animated modal. Let’s get started!
Like I mentioned earlier, react-cool-portal is a Hooks-based abstraction over React portals. With react-cool-portal, creating a portal is as simple as:
const {Portal} = usePortal();
The Hook also comes with various methods and event listeners that allow you to perform more flexible operations. Here are some of the features of react-cool-portal:
<body> or to a specified DOM elementreact and react-domIn this article, we will explore react-cool-portal, and by the end, we will have built an animated modal. Let’s get started!
As I mentioned earlier, you can create a portal with the usePortal Hook. usePortal returns an object that contains the Portal component and some functions:
const {Portal, show, hide, isShow, toggle} = usePortal();
The show function is used to show the portal (mount it) while hide is used to hide it (unmount it). isShow is a Boolean, which returns true when the portal is mounted and false if otherwise. toggle is a function that can be used to show or hide the portal depending on the portal’s current state.
We can provide an argument in the form of an object to our usePortal Hook for further configuration, like so:
const {Portal} = usePortal({
defaultShow: false,
internalShowHide: true,
onShow : e => {
},
onHide: e => {
},
containerId: "portal",
clickOutsideToHide: true,
escToHide: true
});
With defaultShow, we can choose to show or hide our portal’s content by default. internalShowHide enables/disables the show/hide functions of our portal so we can handle the portal however we like.
onShow and onHide are event handlers that are triggered when isShow is set to true or when isShow is set to false, respectively. containerId, which has a default value of react-cool-portal, is used to set the ID of the portal.
clickOutsideToHide defines whether we want to hide the portal by clicking outside of it, while escToHide defines whether we want to hide the portal by clicking the esc key.
That’s basically all you need to know to start using react-cool-portal! Now we will build an animated modal with what we have learned so far.
As with any React project, you have to create a new project before you start working. You can easily use create-react-app for that:
npx create-react-app animated-modal
Once you have created a new project, you need to install react-cool-portal, like so:
npm install --save react-cool-portal
Since this is a pretty small project, I will write all my JavaScript/JSX in one file, App.js, and all my CSS in another file, App.css.
Before we continue, let’s picture what we want to create. We want to have a button that, when clicked, will display an animated modal. We can then exit the modal from the modal itself.
That said, we need to create a piece of state that renders the Portal component depending on whether or not the button has been clicked:
const [showModal, setShowModal] = useState(false);
We also need another piece of state to store the animation state (the different CSS classes that will trigger the animation):
const [animationState, setAnimationState] = useState("");
We should have this currently:
const { Portal, show, hide } = usePortal({
defaultShow: false,
});
const [showModal, setShowModal] = useState(false);
const [animationState, setAnimationState] = useState("");
Now let’s create our simple button element, which will set showModal to true when clicked and which will call the show function.
return (
<div>
<button
onClick={() => {
setShowModal(true);
show();
}}
>
Open Modal
</button>
</div>
);
Now let’s write the modal markup:
return (
<div>
<button
onClick={() => {
setShowModal(true);
show();
}}
>
Open Modal
</button>
{showModal && (
<Portal>
<div className= "modal" tabIndex={-1}>
<div
className={`modal-dialog ${animationState}`}
role="dialog"
aria-labelledby="modal-label"
aria-modal="true"
>
<div className="modal-header">
<h5 id="modal-label">Modal header</h5>
<span
className="modal-exit"
>
close
</span>
</div>
<div className="modal-body">
<p>Modal Body</p>
</div>
</div>
</div>
</Portal>
)}
</div>
);
In our CSS, we are going to have two different animations: slideIn and slideOut. When the button is clicked to open the modal, the animation state is changed to slideIn, and the slideIn class is attached to the modal dialog div.
To do this, we will use the onShow event handler to set the current animation state to slideIn. So anytime the modal is displayed, the slideIn class will be attached to it.
const { Portal, show, hide } = usePortal({
defaultShow: false,
onShow: () => {
setAnimationState("slideIn");
},
});
Our modal has a <span> element that will be used to close the modal when clicked. When this <span> element is clicked, we will set the animation state to slideOut.
<span
className="modal-exit"
onClick={() => {
setAnimationState("slideOut");
}}
>
close
</span>
We will now make use of one of the animation events that React provides: onAnimationEnd. The event handler passed to it will run once the animation has ended.
In our case, once the animation on the modal dialog has ended, we will check the current animation state. If it is slideOut, we will hide the modal. Once that’s done, we will set the animation state to an empty string.
<div
className={`modal-dialog ${animationState}`}
role="dialog"
aria-labelledby="modal-label"
aria-modal="true"
onAnimationEnd={() => {
if(animationState == "slideOut"){
hide();
}
setAnimationState("");
}}
>
Our App component should now look like this:
import React, { useState } from "react";
import "./App.css";
import usePortal from "react-cool-portal";
function App() {
const { Portal, show, hide } = usePortal({
defaultShow: false,
onShow: () => {
setAnimationState("slideIn");
},
});
const [showModal, setShowModal] = useState(false);
const [animationState, setAnimationState] = useState("");
return (
<div>
<button
onClick={() => {
setShowModal(true);
show();
}}
>
Open Modal
</button>
{showModal && (
<Portal>
<div className= "modal" tabIndex={-1}>
<div
className={`modal-dialog ${animationState}`}
role="dialog"
aria-labelledby="modal-label"
aria-modal="true"
onAnimationEnd={() => {
if(animationState == "slideOut"){
hide();
}
setAnimationState("");
}}
>
<div className="modal-header">
<h5 id="modal-label">Modal header</h5>
<span
className="modal-exit"
onClick={() => {
setAnimationState("slideOut");
}}
>
close
</span>
</div>
<div className="modal-body">
<p>Modal Body</p>
</div>
</div>
</div>
</Portal>
)}
</div>
);
}
That’s it for the JavaScript — let’s move on to the the CSS. This is the CSS for the modal:
body{
--curve: cubic-bezier(0.22, 1, 0.36, 1);
}
#react-cool-portal{
position: absolute;
top:0;
left: 0;
min-width: 100vw;
height: 100%;
}
.modal{
height: 100%;
width: 100%;
display: flex;
z-index: 20;
justify-content: center;
align-items: center;
background-color: rgba(0,0,0,0.7);
}
.modal-dialog{
background-color: white;
border-radius: 10px;
width: 80%;
max-width: 400px;
padding: 1rem;
}
.modal-header{
font-weight: 400;
font-size: 1.5rem;
display: flex;
justify-content: space-between;
}
.modal-header #modal-label{
margin:0;
}
.modal-exit{
font-size: 1rem;
color: red;
cursor: pointer;
}
.slideIn{
animation: slideIn 0.5s var(--curve) 0s 1 normal none;
}
.slideOut{
animation: slideOut 0.5s var(--curve) 0s 1 normal forwards;
}
@keyframes slideIn {
0% {
transform: translateY(-2rem);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
@keyframes slideOut {
100% {
transform: translateY(-2rem);
opacity: 0;
}
0% {
transform: translateY(0);
opacity: 1;
}
}
You should now have a nicely animated modal!
With that, we’re done! You can create a custom Hook called useModal based on react-cool-portal for code reusability.
The link to the repo for this project can be found here. You can find the live demo here. And, finally, you can learn more about react-cool-portal here.
Install LogRocket via npm or script tag. LogRocket.init() must be called client-side, not
server-side
$ npm i --save logrocket
// Code:
import LogRocket from 'logrocket';
LogRocket.init('app/id');
// Add to your HTML:
<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>

Vibe coding isn’t just AI-assisted chaos. Here’s how to avoid insecure, unreadable code and turn your “vibes” into real developer productivity.

GitHub SpecKit brings structure to AI-assisted coding with a spec-driven workflow. Learn how to build a consistent, React-based project guided by clear specs and plans.

:has(), with examplesThe CSS :has() pseudo-class is a powerful new feature that lets you style parents, siblings, and more – writing cleaner, more dynamic CSS with less JavaScript.

Kombai AI converts Figma designs into clean, responsive frontend code. It helps developers build production-ready UIs faster while keeping design accuracy and code quality intact.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now
One Reply to "React-cool-portal: What it is and how to use it"
There are more very interesting portal helpers coming up https://github.com/codastic/react-positioning-portal