The goal
Create a React modal dialog box using the html "div" element. Content is being provided as children. (Compare to: React modal using an html "dialog")
Notes
This "Modal" component is generic: the modal content is contained inside the parent container, the modal only wraps it.
In general I strongly suggest you create a modal conditionally like here (see:
isModalOpen &&
below), instead of creating it always but displaying it conditionally. I have seen websites with a large number of modals on their main page, where each modal content was heavy using a significant amount of cpu and memory; on a typical session only a couple of these modals were opened, though the page totally unnecessarily created all of them every time and ended up being very large and slow.
Component 1 - the "ModalTester" contains and opens the modal
import { useState } from "react";
import styled from "styled-components";
import { Modal } from ".";
const SomeContentForTheModal = styled.div`
height: 180px;
display: flex;
flex-direction: column;
justify-content: center;
gap: 20px;
`;
// ------------------------------------
const ModalTester = () => {
const [isModalOpen, setModalOpen] = useState(false);
return (
<div data-testid="ModalTester">
<button onClick={() => setModalOpen(true)}>Open the modal</button>
{isModalOpen && (
<Modal
title="Modal example"
proceedButtonText="Proceed"
onProceed={() => console.log("Proceed clicked")}
onClose={() => setModalOpen(false)}
>
<SomeContentForTheModal>
<p>This is the modal content.</p>
<p>To close: click Cancel, press Escape, or click outside.</p>
</SomeContentForTheModal>
</Modal>
)}
</div>
);
};
export default ModalTester;
Component 2 - the "Modal" component itself
import { ReactNode } from "react";
import styled from "styled-components";
import { Button } from "../components";
import { useKeyDown } from "../hooks";
const Overlay = styled.div`
z-index: 101;
position: fixed;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
`;
const Container = styled.div`
z-index: 102;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 8px;
padding: 0;
background-color: var(--color-whitish);
`;
const TopSection = styled.div`
position: relative;
padding: 0 20px;
height: 60px;
display: flex;
align-items: center;
justify-content: space-between;
background-color: var(--color-gray-lighter);
border-radius: 8px 8px 0 0;
`;
const MiddleSection = styled.div`
position: relative;
padding: 0 20px;
`;
const BottomSection = styled(TopSection)`
border-radius: 0 0 8px 8px;
`;
const FlexSpace = styled.div`
flex-grow: 1;
`;
// ------------------------------------
type Props = {
title: string;
proceedButtonText?: string;
closeButtonText?: string;
onProceed?: () => void;
onClose: () => void;
children: ReactNode;
};
// ------------------------------------
const Modal = ({
title,
proceedButtonText,
closeButtonText,
onProceed,
onClose,
children,
}: Props) => {
useKeyDown("Escape", onClose);
const proceedAndClose = () => {
onProceed?.();
onClose();
};
// Prevents closing when we click inside the modal
const preventAutoClose = (e: React.MouseEvent) => e.stopPropagation();
// ------------------------------------
return (
<Overlay onClick={onClose}>
<Container onClick={preventAutoClose}>
<TopSection>
<h3>{title}</h3>
</TopSection>
<MiddleSection>{children}</MiddleSection>
<BottomSection>
{!!proceedButtonText && (
<Button text={proceedButtonText} onClick={proceedAndClose} />
)}
<FlexSpace />
<Button text={closeButtonText || "Cancel"} onClick={onClose} />
</BottomSection>
</Container>
</Overlay>
);
};
export default Modal;
Additionally a useKeyDown() hook closes the modal if the Escape gets pressed
import { useEffect } from "react";
const useKeyDown = (key: string, onKeyDown: () => void) => {
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === key) onKeyDown();
};
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
};
export default useKeyDown;
My recommendation
Personally I clearly prefer this React modal here which uses the html "div", and not the one using the new html "dialog" element. I think:
- a modal implemented using a "div" is: simpler, more intuitive, safer, and easier to maintain/troubleshoot,
- the new html "dialog" element is unnecessarily complicated.
Thanks for reading. Suggestions/corrections are welcome.
Top comments (0)