How To Create A Modal UI In React
![How To Create A Modal UI In React](https://d19d9m2dcgrxq7.cloudfront.net/how-to-create-a-modal-ui-in-react.png)
How To Create A Modal UI In React
Modals are one of those ubiquitous UI elements in modern application development. At first glance, it may seem a simple thing to build. Given all the different component libraries and open source packages that provide a solution for this UI element, one should probably not bother with this tricky task without good reason.
Perhaps your application’s codebase cannot support another component library. Perhaps the team refuses to import yet another npm package for just a modal component. Perhaps, after all, you’re just curious about how to do it in React.
They seem simple at first blush because of how we use them. There’s a button on the page, and a user clicks it. That opens a modal with a message or a form. In the corner, the modal has a close icon, and at the bottom, a pair of buttons; “cancel” and “confirm.”
But what happens when the user is tabbing through the elements in the modal? Where does the focus go once the modal closes? How should we handle the Modal component’s state in the React application? How can we make the component’s API easy to use and repeatable?
Installing & running a new React App
Enter into an empty directory. We will start downloading some basic code here.
$ yarn create react-app react-modal-example
$ cd react-modal-example
Running the line above will let Yarn use create-react-app to create a brand new blank React application and cd into our working directory.
$ yarn start
This will start up your new React app. By default, your app will run on localhost, port 3000: localhost:3000.
The Modal dialog itself.
Let’s consider the open modal a bit before diving into how to design it in the context of React.
Generally speaking, the modal UI brings a simple box into focus for the user. The box is normally centered and contains a simple escape button, some kind of cancel button, and some kind of confirm button. In the center of all that, we also need to allow for custom content that the developer wants to place inside the modal.
Modal UIs normally also gray out the background content to bring it into focus. And if the user clicks this, the Modal should close.
Great, so putting these into focus, we might have something like this:
<div
style={{
alignItems: 'center',
bottom: 0,
display: open ? 'flex' : 'none',
justifyContent: 'center',
position: 'fixed',
top: 0,
left: 0,
right: 0,
}}
>
<div
onClick={handleClose}
style={{
backgroundColor: 'rgba(0,0,0,0.25)',
bottom: 0,
position: 'absolute',
top: 0,
left: 0,
right: 0,
}}
/>
<div
style={{
backgroundColor: 'white',
padding: '1rem',
position: 'relative',
}}
>
<button
onClick={handleClose}
style={{ backgroundColor: 'red', cursor: 'pointer' }}
>
<span className="sr-only">close modal</span>
<MdClose />
</button>
// custom content here
<div style={{ color: 'white', display: 'flex', justifyContent: 'space-evenly' }}>
<button
onClick={handleClose}
style={{ backgroundColor: 'red', cursor: 'pointer' }}
>cancel</button>
<button
onClick={handleConfirm}
style={{ backgroundColor: 'green', cursor: 'pointer' }}
>confirm</button>
</div>
</div>
</div>
The next thing we need to tackle is how to make sure the Modal content is always rendered on top of our application. Luckily React offers a neat method called createPortal. It lets us render a React element into a specific DOM node instead of as a child of the component it’s called in.
To make sure we’re being purposeful with this API, let's create a special container for our Modal elements. Inside the index.html found inside the public folder, add a div tag with the id “modal-root”:
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<div id="modal-root"></div>
</body>
This will reduce styling blunders and the need for any z-indexing entanglements since modals will always be rendered last – “on top of” the above elements – when using relative positioning.
By passing in props to handle closing, confirming, the different optional labels for our buttons, and the custom elements to render inside the Modal, we might have the following (with some lines removed for brevity):
// src/components/Modal/index.js
import { createPortal } from 'react-dom';
import { MdClose } from 'react-icons/md';
const Modal = ({
children,
handleClose,
handleConfirm,
labels,
open,
}) => {
return createPortal(
<div style={{...}}>
<div
onClick={handleClose}
style={{...}}
/>
<div style={{...}}>
<button
onClick={handleClose}
style={{ backgroundColor: 'red', cursor: 'pointer' }}
>
<span className="sr-only">close modal</span>
<MdClose />
</button>
{ children }
<div style={{ color: 'white', display: 'flex', justifyContent: 'space-evenly' }}>
<button
onClick={handleClose}
style={{ backgroundColor: 'red', cursor: 'pointer' }}
>{ labels?.cancel ?? 'cancel' }</button>
<button
onClick={handleConfirm}
style={{ backgroundColor: 'green', cursor: 'pointer' }}
>{ labels?.confirm ?? 'confirm' }</button>
</div>
</div>
</div>,
document.getElementById('modal-root')
);
};
export default Modal;
We could add more, but let’s leave this as-is for now.
With what we have, we could use this simple Modal component and build off from this easily. But then we would need to build out the Modal’s open states, the close and confirm handlers. We would also need to build a companion button to trigger the opening of the Modal. Every time we use this component, we would need to do all of the above, even if the only thing we want to do is display a message to the user and a close button.
Let's DRY (don’t repeat yourself) up some of that repetition and build a custom hook to make this even easier to use.
Making the Modal component more scalable and faster to use.
Our custom hook, at minimum, will always need to manage the Modal’s open state and provide the consumer with both the Modal’s open button and the Modal itself. It should accept callback functions for opening, closing, and confirming the Modal, as well as accept any custom labels.
Basically, we should be able to call our custom hook like so:
const [Button, Modal] = useModal({/* config options go here */});
And remember, since our Modal component uses createPortal, we can place it wherever we like, and it will always render inside our designated div element.
Based on what we’ve discussed, try to develop this custom hook on your own. When you’ve finished, it should look something like this:
// src/components/Modal/useModal.js
import { useState } from 'react';
import ModalDialog from './index';
const useModal = ({ onModalOpen, onModalClose, onConfirm, labels }) => {
const [open, setOpen] = useState(false);
const handleClose = () => {
setOpen(false);
onModalClose?.();
};
const handleOpen = () => {
setOpen(true);
onModalOpen?.();
};
const handleConfirm = () => {
onConfirm?.();
handleClose();
};
const Modal = ({ children }) => (
<ModalDialog
children={children}
handleClose={handleClose}
handleConfirm={handleConfirm}
labels={labels}
open={open}
/>
);
const Button = ({ children }) => (
<button
onClick={handleOpen}
>{ children }</button>
);
return [
Button,
Modal,
]
};
export default useModal;
Great! Now we have a simple custom hook that we can call whenever we have a component that requires a Modal UI element. Let’s see how it turned out.
Putting it all together
First, let’s build a simple modal that uses our reusable Modal component.
// src/components/SimpleModal/index.js
import useModal from '../Modal/useModal';
const SimpleModal = () => {
const [Button, Modal] = useModal({});
return (
<>
<Button>open simple modal</Button>
<Modal>
<p>Simple message for a simple modal!</p>
</Modal>
</>
);
};
export default SimpleModal;
Then import it into App.js:
import SimpleModal from './components/SimpleModal';
import './App.css';
function App() {
return (
<div className="App">
<h1>Accessible Modals in React</h1>
<SimpleModal />
</div>
);
}
export default App;
Now your app should render an extremely simple modal component. While this article doesn’t favor the styling of this UI, please add your transitions and styling to play with a design you're happy with.
Of course, we should also make sure that our configuration works as expected. Update the SimpleModal component with the following functions and update the useModal’s config:
const onConfirm = () => console.log('confirm!');
const onModalClose = () => console.log('modal closed!');
const onModalOpen = () => console.log('modal opened!');
const [Button, Modal] = useModal({ onConfirm, onModalOpen, onModalClose });
Trapping keyboard focus
Now the consumer of this Modal may wish to render some input elements inside their modal. Perhaps a complete form! Of course, many of their users may depend on the browser’s native focus capabilities with these elements, and that’s the rub.
Currently, our custom Modal component would allow the user to focus out of the modal. This is no good! We want to keep their attention inside the modal.
To accomplish this, we will add an event handler to our last focusable element and let that handler focus on our first focusable element.
<button
onClick={handleConfirm}
onKeyDown={handleFocus}
style={{ backgroundColor: 'green', cursor: 'pointer' }}
>{ labels?.confirm ?? 'confirm' }</button>
So when the user is focused on the confirm button and they press a key, the handleFocus function will be called. When this is called, how do we focus on the close button (our first focusable element in the container)?
We need a reference to this element, so we employ useRef and create our handleFocus function accordingly.
const closeButtonRef = useRef();
const handleFocus = (e) => {
if (e.key !== 'Tab') return;
if (!closeButtonRef.current) return;
closeButtonRef.current.focus();
e.preventDefault();
};
...
<button
onClick={handleClose}
ref={(el) => closeButtonRef.current = el}
style={{ backgroundColor: 'red', cursor: 'pointer' }}
>
<span className="sr-only">close modal</span>
<MdClose />
</button>
Now no matter what our fellow developers nest inside of our Modal component, the focus will remain trapped until the Modal is closed.
Summary
Here, we’ve learned the basic elements of a Modal UI and how to implement one with React thanks to the library’s useRef, custom hook, and createPortal APIs.
Of course, there are more details to discuss when thinking about this Modal component. What if we don’t need both the cancel and confirm buttons? (Hint: pass in an optional flag property) How do we return focus to the page once the modal is closed? (Hint: pass in a ref to another element on the page and let your Modal component handle the rest when it closes).
Regardless, this is a good starting place to explore what we can do with Modal components in React. Also, keep an eye out for the native HTML dialog element. It’s still not perfect as of this writing, but in the future will handle many of our accessibility concerns when building a Modal component.