Modals, for better or worse are an often requested feature in web applications. I recently ran across a pattern that allows for managing modal state and placement in a React application that not only works, but feels OK
to implement. The use of a custom hook allows the management of modal state without relying on a state management lib and without polluting your component or application state. React Portals allow us to attach components anywhere we want in our application. In this example, we will hoist the component completely out of our component's parent scope and append it to the body
element.
useModal.ts
useModal.ts
is a custom hook that manages the visibility of our modal. The hook returns the visibility of the modal and a toggleVisibility function that does exactly what the name implies.
import React from "react";
export const useModal = () => {
const [modalIsVisible, setModalIsVisible] = React.useState(false);
const toggleModalVisibility = () => setModalIsVisible(!modalIsVisible);
return [modalIsVisible, toggleModalVisibility] as const;
};
Modal.tsx
Modal.tsx
is the modal component. Notes:
- The custom
useModal
hook gives us access to the state of the modal from within the modal itself and allows us to toggle visibility by passing thetoggleVisibility
function into our modal UI. -
ReactDOM.createPortal
allows us to hoist the modal component outside of the scope of it's parent node and attach it to thebody
of our application.
import React from "react";
import ReactDOM from "react-dom";
type ModalProps = {
isVisible: boolean;
toggleVisibility: () => void;
modalContent: React.ReactNode;
};
export const Modal = ({
isVisible,
toggleVisibility,
}: Readonly<ModalProps>): JSX.Element | null => {
const modal: JSX.Element = (
<>
<div className="backdrop" onClick={toggleVisibility} />
<div className="modal" aria-modal aria-label="Modal Details" role="dialog">
{modalContent}
<span
className="modal-close"
aria-label="Close Modal Details"
onClick={toggleVisibility}
>
×
</span>
</div>
</>
);
return isVisible ? ReactDOM.createPortal(modal, document.body) : null;
};
modal-styles.css
CSS is needed to display the modal correctly. Styles will be incredibly application-dependent, but I usually start with some fixed positioning and a close button in the top right corner.
.backdrop {
background-color: rgba(255, 255, 255, 0.6);
bottom: 0;
left: 0;
position: fixed;
right: 0;
top: 0;
}
.modal {
--var-rhythm: 1.8rem;
align-items: center;
background-color: white;
border: 1px solid gray;
border-radius: 6px;
display: flex;
flex-direction: column;
justify-content: center;
left: 50%;
max-width: calc(100vw - var(--rhythm));
max-height: calc(100vh - var(--rhythm));
min-width: 300px;
padding: calc(var(--rhythm) * 2) calc(var(--rhythm) * 2) var(--rhythm);
position: fixed;
text-align: center;
top: 50%;
transform: translate(-50%, -50%);
overflow-y: scroll;
}
@media (min-width: 600px) {
.modal {
min-width: 600px;
}
}
.modal > * {
margin: 0;
margin-bottom: var(--rhythm);
}
.modal-close {
color: gray;
cursor: pointer;
font-size: 2rem;
line-height: 1rem;
padding: 0;
position: absolute;
right: calc(var(--rhythm) / 2);
top: calc(var(--rhythm) / 2);
}
Component.tsx
Now, all that is needed to use our modal is to import the hook and Modal.tsx
anywhere we need it in our application.
import { Modal } from "../components/Modal";
import { useModal } from "../../hooks/useModal";
export const Component = (): JSX.Element => {
const [modalIsVisible, toggleModalVisibility] = useModal();
const modalContent: React.ReactNode = (<p>This goes in the modal.</p>);
return (
<Modal
isVisible={modalIsVisible}
toggleVisibility={toggleModalVisibility}
modalContent={modalContent}
/>
)
};
Have fun making modals ಠ_ಠ! If you have a better pattern for implementing them I would love to be schooled... keep learning!
Top comments (4)
You can check mine
github.com/franlol/useModal
I will check it out, thanks!
Really like this pattern Nate!
Extending off this idea, I've been wondering about making a global Modal provider & context. But I'm not sure how much value it would add.
We are driving some of our global UI state with Apollo
useReactiveVar
now. It's handy and already ships with Apollo. I wouldn't suggest mixing your global state management though, so context works if you are already using it :)