Building a fullscreen overlay with React 16s portal
Thomas Maximini / February 12, 2018
4 min read
Recently I found myself once again in the situation that I had to build a fullscreen overlay for a website, in this case for displaying a video. This is probably something every web developer encounters on a regular basis. As with most programming problems there are many ways to solve this - but as I was reading React 16's changelog recently I thought why giving the newly-added Portals a shot.
What is a Portal, really?
A portal is just a DOM-fragment that is not mounted as a direct child or sibling to the current component, but can reside on a completly different part on the DOM. As the docs state Portals provide a first-class way to render children into a DOM node that exists outside the DOM hierarchy of the parent component.
Let's build the Overlay component
First, let's build a very generic and re-usable Modal
component that implements the Portal logic.
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
// I use the same div here that I mount my app into
// so the modal will be a sibling of the rest of the app
// in the DOM hierachy
const modalRoot = document.getElementById('root');
export default class Modal extends React.PureComponent {
static propTypes = {
children: PropTypes.node,
};
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);
}
}
This component does nothing else than creating a div and mouting it as a child of #root
into our DOM.
Now we build the actual VideoModal
component that makes use of this generic Modal and adds a little functionality:
import React from 'react';
import PropTypes from 'prop-types';
import CssModules from 'react-css-modules';
import { RemoveIcon } from '@components/Shared/Icons/Icons';
import Modal from './Modal';
import classes from './VideoModal.scss';
@CssModules(classes)
export default class VideoModal extends React.PureComponent {
static propTypes = {
children: PropTypes.node,
handleClose: PropTypes.func.isRequired,
};
render() {
return (
<Modal>
<div styleName="wrapper">
<div styleName="inner">
<button onClick={this.props.handleClose} styleName="close">
<RemoveIcon />
</button>
{this.props.children}
</div>
</div>
</Modal>
);
}
}
The two keyparts here is to render the actual children of this component as well as passing a handleClose
function that is called when the 'X' or RemoveIcon
is clicked.
Here is the CSS of this component:
.wrapper {
position: fixed;
left: 0;
top: 0;
background-color: rgba(0, 0, 0, 0.95);
height: 100vh;
width: 100vw;
z-index: 999;
}
.inner {
position: relative;
width: 100%;
height: 100%;
text-align: center;
display: flex;
align-items: center;
justify-content: space-around;
}
.close {
position: absolute;
top: 20px;
right: 20px;
color: white;
background: none;
border: 0;
}
Usage
Now we can use the Component, for example to display fullscreen images or videos after clicking on an item in a list.
Imagine a video list component where we map over an array of items like this:
// rest of component omitted ...
render() {
const { stories } = this.props;
return (
<div id="video-stories" styleName="wrapper">
{stories.map((story) => <StoryAvatar key={story.id} story={story} />)}
</div>
);
}
Now, the StoryAvatar
might look something like this:
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import VideoModal from '../Modal/VideoModal';
export default class StoryAvatar extends PureComponent {
static propTypes = {
story: PropTypes.object.isRequired,
};
state = {
shown: false,
};
handleClick = () => {
const { shown } = this.state;
this.setState({
shown: !shown,
});
};
renderModal = () => (
<VideoModal handleClose={this.handleClick}>
<iframe
title="test"
src="https://player.vimeo.com/video/253742573"
width="640"
height="360"
frameBorder="0"
allowFullScreen
/>
</VideoModal>
);
render() {
const { story } = this.props;
const { seen } = this.state;
return (
<div styleName={classnames('story', { seen })}>
<span styleName="button" onClick={this.handleClick}>
<span styleName="img">
<span
styleName="avatar"
style={{
backgroundImage: `url(${story.photo})`,
}}
/>
</span>
<span styleName="info">
<span styleName="videoname">{story.videotitle}</span>
<span styleName="username">{story.name}</span>
</span>
</span>
{shown && this.renderModal()}
</div>
);
}
}
Here we are using the component's state to decide if a fullscreen overlay is displayed or not. The handleClick
method toggles the shown
parameter in the state, which in return is used to conditionally call this.renderModal
.