Editor’s note: This article was reviewed and updated by Rahul Chhodde in January 2025. The updates include revisions to code blocks, ensuring compatibility with React 19, and the removal of outdated practices, such as specifying the React.FC type for functional components. It is now recommended to define prop types directly, allowing React to infer the return type based on these specifications.
A pop-up modal is a crucial UI element you can use to engage with users when you need to collect input or prompt them to take action. Advancements in frontend web development have made it easier than ever to incorporate modal dialogs into your apps.
In this article, we will focus on utilizing the native HTML5 <dialog> element to construct a reusable pop-up modal component in React. You can check out the complete code for our React pop-up modal in this GitHub repository.
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.
A modal dialog is a UI element that temporarily blocks a user from interacting with the rest of the application until a specific task is completed or canceled. It overlays the main interface and demands the user’s attention.
A typical example of a modal is an email subscription box frequently found on blog websites. Until the user responds by subscribing or dismissing the modal, they cannot interact with the underlying content in the main interface. Other examples include login or signup dialogs, file upload boxes, file deletion confirmation prompts, and more.
Modals are useful for presenting critical alerts or obtaining important user input. However, they should be used sparingly to avoid disrupting the user experience unnecessarily.
Non-modal dialogs, in contrast to modal dialogs, allow users to interact with the application while the dialog is open. They are less intrusive and do not demand immediate attention.
Some examples of non-modal dialogs are site preference panels, help dialogs, cookie consent dialogs, context menus — the list goes on.
This article is primarily focused on modal dialogs.
<dialog> elementBefore the native HTML <dialog> element was introduced, developers had to rely solely on JavaScript to add the modal functionality to HTML divs.
However, the native HTML <dialog> element is now widely supported on modern browsers. Thanks to the JavaScript HTMLDialogElement Web API — designed specifically for the <dialog> element — modal dialogs have become more semantically coherent and easier to handle.
The native dialog element comes packed with accessibility features such as the dialog role and the modal aria attribute. These bits of info are read automatically by assistive technologies like screen readers, enhancing the overall accessibility of modals you create using <dialog>.
This also means that you don’t necessarily need a third-party library to construct pop-up modals, and can use the native dialog element to do the same with fewer lines of code.
HTMLDialogElement APILet’s explore the essential markup for constructing a modal using the <dialog> element:
<button id="openModal">Open the modal</button> <dialog id="modal" class="modal"> <button id="closeModal" class="modal-close-btn">Close</button> <p>...</p> <!-- Add more elements as needed --> </dialog>
Note that the modal component can be set to open by default by including the open attribute within the <dialog> element in the markup:
<dialog open> ... </dialog>
We can now utilize the JavaScript HTMLDialogElement API to control the visibility of the modal component that we previously defined. It’s a straightforward process that involves obtaining references to the modal itself along with the buttons responsible for opening and closing it.
By utilizing the showModal and close methods provided by the HTMLDialogElement API, we can easily establish the necessary connections:
const modal = document.querySelector("#modal");
const openModal = document.querySelector("#openModal");
const closeModal = document.querySelector("#closeModal");
openModal?.addEventListener('click', () => {
modal?.showModal();
});
closeModal.addEventListener('click', () => {
modal?.close();
});
Note that if we use dialog.show() instead of dialog.showModal(), our <dialog> element will behave like a non-modal element.
Take a look at the following implementation. It may be simple, but it is fully functional. It is also much easier to integrate and provides greater semantic value than a comprehensive modal solution built entirely with JavaScript:
See the Pen
The native <dialog> modal: A basic example by Rahul (@_rahul)
on CodePen.
<dialog> elementA modal interface powered by the HTML <dialog> element is easy to style and has a special pseudo-class that makes modal elements simple to select and style. I’ll keep the styling part simple for this tutorial and focus more on the basics before delving into the React implementation.
:modal pseudo-classThe :modal CSS pseudo-class was specifically designed for UI elements with modal-like properties. It enables easy selection of a dialog displayed as a modal and the application of appropriate styles to it:
dialog {
/* Styles for dialogs that carry both modal and non-modal behaviors */
}
dialog:modal {
/* Styles for dialogs that carry modal behavior */
}
dialog:not(:modal) {
/* Styles for dialogs that carry non-modal behavior */
}
The choice between these approaches — selecting an element directly to set defaults, selecting its states to apply state-specific styles, or using CSS classes to style the components — is entirely subjective.
Each method offers different advantages, so the most suitable approach for styling will depend on the developer’s preference and the project’s operating procedure. I’ll go the CSS classes route to style our modal.
Let’s enhance it by incorporating rounded corners, spacing, a drop shadow, and some layout properties. You can add or customize these properties according to your specific needs:
.modal {
position: relative;
max-width: 20rem;
padding: 2rem;
border: 0;
border-radius: 0.5rem;
box-shadow: 0 0 0.5rem 0.25rem hsl(0 0% 0% / 10%);
}
Additionally, we’ll position the Close button in the top right corner so that it doesn’t interfere with the modal content. Furthermore, we’ll set some default styles for the buttons and input fields used in our application:
.modal-close-btn {
font-size: .75em;
position: absolute;
top: .25em;
right: .25em;
}
input[type="text"],
input[type="email"],
input[type="password"],
button {
padding: 0.5em;
font: inherit;
line-height: 1;
}
button {
cursor: pointer;
}
::backdrop pseudo-elementWhen using traditional modal components, a backdrop area typically appears when the modal is displayed. This backdrop acts as a click trap, preventing interaction with elements in the background and focusing solely on the modal component.
To emulate this functionality, the native <dialog> element introduces the CSS ::backdrop pseudo-element. Here’s an example illustrating its usage:
.modal::backdrop {
background: hsl(0 0% 0% / 50%);
}
The user agent style sheet will automatically apply default styles to the backdrop pseudo-element of dialog elements with a fixed position, spanning the full height and width of the viewport.
The backdrop feature will not function for non-modal dialog elements, as this type of element allows users to interact with the underlying content while the dialog is open.
The following example showcases an example implementation of all the aforementioned styling. Click the Open the modal button to observe the functionality:
See the Pen
The native <dialog> modal: CSS Styling by Rahul (@_rahul)
on CodePen.
Notice how the previously mentioned backdrop area works. When the modal is open, you’re not able to click on anything in the background until you click the Close button.
<dialog> elementNow that we understand the basic HTML structure and styles of our pop-up modal component, let’s transfer this knowledge to React by creating a new React project.
In this example, I’ll be using React with TypeScript, so the code provided will be TypeScript-specific. However, I also have a JavaScript-based demo of the component we are about to build that you can reference if you are using React with JavaScript instead.
Once the React project is set up, let’s create a directory named components. Inside this directory, create a subdirectory called Modal to manage all of our Modal dialog component files. Now, let’s create a file inside the Modal directory called Modal.tsx:
import { useRef } from "react";
const Modal = () => {
const modalRef = useRef<HTMLDialogElement>(null);
return (
<dialog ref={modalRef} className="modal">
{children}
</dialog>
);
}
export default Modal;
In the above code snippet, we simply defined the Modal component and used the useRef Hook to create a reference to the HTML <dialog> element that we could use later in the useEffect Hooks.
N.B., instead of implementing a React.FC<MyProps> return type on components in the examples ahead, I have specified direct typing (e.g., props: MyProps) for component props, which helps React infer return types automatically. This is now the recommended way to structure your components.
To make this component work, we need to consider the following points to determine the props we will need:
props.children property provided by ReactThe above points contribute to shaping the type structure of our props, which we will construct using the TypeScript interface as illustrated below:
interface ModalProps {
isOpen: boolean;
hasCloseBtn?: boolean;
onClose?: () => void;
children: React.ReactNode;
};
Some properties in the above type are optional to avoid additional setup — such as showing a close button in the modal or executing something on closing the dialog — if not needed.
The Modal component will be used for presentational purposes while also utilizing the native HTML5 Dialog API to manage modal visibility. Instead of managing its internal state directly, it receives body and actions through its props and uses the Dialog API methods to control its functioning. This approach not only keeps Modal simple to implement but also truly reusable in a variety of cases.
With the basic structure of our Modal component in place, we can now proceed to implement the functionality for opening the modal.
ModalThe useEffect Hook is ideal for keeping things in sync because it enables performing side effects, such as updating states or interacting with APIs, in response to changes in specific dependencies.
As already discussed, we are not dealing with state variables here. Instead, we will interact with the HTMLDialogElement API within a useEffect block, which re-runs whenever the isOpen prop changes. This will ensure that the visibility of our Modal stays in sync with isOpen, allowing the component to respond accurately to external changes.
To implement the HTML <dialog> modal with React, we will first grab the reference to our modal element in the DOM using the useRef Hook. If an occurrence is found, we will conditionally switch the modal’s visibility based on the value of isOpen by utilizing Dialog.showModal() and Dialog.close(). Here’s what it will look like:
useEffect(() => {
// Grabbing a reference to the modal in question
const modalElement = modalRef.current;
if (!modalElement) return;
// Open modal when `isOpen` changes to true
if (isOpen) {
modalElement.showModal();
} else {
modalElement.close();
}
}, [isOpen]);
ModalWe should now create a utility function that incorporates the optional onClose callback. This function can be used later to easily close the Modal dialog in different scenarios:
const handleCloseModal = () => {
if(onClose) {
onClose();
}
};
If you observe closely, the ability to close the modal by pressing the escape key is an inherent feature of the HTML5 <dialog> element.
However, because our Modal component depends on the isOpen Boolean to operate, it will malfunction after being closed by pressing the escape key because the value of isOpen won’t be updated correctly. To ensure its proper functioning, we should fire handleCloseModal whenever the escape key is pressed.
To achieve this, we can set up a simple handler function to invoke the handleCloseModal function whenever the event (KeyboardEvent) corresponds to the escape key:
const handleKeyDown = (event: React.KeyboardEvent<HTMLDialogElement>) => {
if (event.key === "Escape") {
handleCloseModal();
}
};
This approach ensures that the modal is closed appropriately when the user presses the escape key, triggering the logic we wrote in the useEffect block of the last segment where we utilized the HTMLDialogElement API methods.
In the final steps, we will utilize the optional hasCloseBtn prop to include a close button inside the Modal component. This button will be linked to handleCloseModal action, which is designed to close the modal as expected.
We will also implement the handleKeyDown function and associate it with the onKeyDown event handler for the main HTML5 <dialog> element that will be returned by the Modal component.
See the code below:
return (
<dialog ref={modalRef} onKeyDown={handleKeyDown}>
{hasCloseBtn && (
<button className="modal-close-btn" onClick={handleCloseModal}>
Close
</button>
)}
{children}
</dialog>
);
Accessibility note: When implementing an icon-only close button with no text, make sure to provide a meaningful label with the aria-label attribute to help screen readers figure out the behavior of the button.
With these updates, our React Modal component is now fully functional and complete, making use of the powerful HTML5 <dialog> element and its JavaScript API.
Modal componentNow, let’s put the modal dialog component to use and observe its functionality.
Using the Modal component is pretty straightforward now. We can import the component and start using it as shown in the following code:
import { useState } from "react";
const App = () => {
const [isModalOpen, setModalOpen] = useState<boolean>(false);
return (
<div className="App">
<button onClick={() => setModalOpen(true)}>Open Modal</button>
<Modal
isOpen={isModalOpen}
onClose={() => setModalOpen(false)}
hasCloseBtn={true}>
<h1>Hey</h1>
</Modal>
</div>
);
};
As you can see, the parent component manages the Modal component’s opened and closed state and also provides a content body to it.
Let’s cover a more complex example where we construct one of the commonly seen modal UI elements on the web: a newsletter subscription modal dialog. This modal will include some form fields and invite the visitor to sign up for a newsletter subscription.
The purpose of developing this specific component is to showcase the versatility of the modal pattern for creating various types of modals.
As established in the previous section, we kept our Modal component state-free and built it to manage its visibility using the dialog API methods. Our NewsletterModal component will implement the Modal component and handle the state for its form elements with the useState Hook.
With states, we will demonstrate how to manage data in our components, pass it on to the frontend, or toss it over to the backend/database through an API call. For simplicity’s sake, I’m using the useState Hook provided natively by React.
If you’re new to React, choosing the Context API or a dedicated state management library comes with a learning curve. A good starting point is our guides on managing React states with Context API, Redux, and Zustand.
NewsletterModal componentThe plan is to create an additional component responsible for managing the form and its data within our newsletter modal dialog. To achieve this, let’s create a new subdirectory named NewsletterModal under the components directory.
Within the NewsletterModal directory, create a new file called NewsletterModal.tsx, which will serve as our NewsletterModal component. Optionally, you can also add a NewsletterModal.css file to style the component according to your requirements.
Let’s begin by importing some essential dependencies, including our Modal component that we finished in the previous section:
import React, { useState, useEffect, useRef } from 'react';
import './NewsletterModal.css';
import Modal from '../Modal/Modal';
Our newsletter form will comprise two input fields — one to collect the user’s email and the other to allow users to choose their newsletter frequency preferences. We’ll include monthly, weekly, or daily options in the latter field.
To achieve this, we’ll once again utilize TypeScript interfaces. We’ll also export this interface to reuse it in the main App component:
export interface NewsletterModalData {
email: string;
digestType: "daily" | "weekly" | "monthly";
}
Next, we’ll define the props that our NewsletterModal component will receive:
interface NewsletterModalProps {
isOpen: boolean;
modalData: NewsletterModalData;
onSubmit: (data: NewsletterModalData) => void;
onClose: () => void;
}
As you can see, the NewsletterModal component expects three props:
isOpen — A Boolean indicating whether the modal is open or notmodalData — NewsletterModalData values to populate ModalonSubmit — A function that will be called when the form is submitted. It takes a property of the NewsletterModalData type as an argumentonClose — A function that will be called when the user closes the modalTwo of these props, namely isOpen and onClose, will further be used as prop values for the Modal component.
NewsletterModal componentNow, let’s define the actual NewsletterModal component. It’s a functional component that takes in the props defined in the NewsletterModalProps interface. We use object destructuring to extract these props:
const NewsletterModal = ({
isOpen,
modalData,
onClose,
onSubmit,
}: NewsletterModalProps) => {
// Component implementation goes here...
};
Next, we use the useRef Hook to create a reference to the input element for the email field. This reference will be used later to focus on the email input when the modal is opened.
We also use the useState Hook to create a state variable to manage the form data, initializing it with initialNewsletterModalData.
See the code below:
const focusInputRef = useRef<HTMLInputElement>(null); const [formData, setFormData] = useState<NewsletterModalData>(modalData);
To handle side effects when the value of isOpen changes, we utilize the useEffect Hook. If isOpen is true and the focusInputRef is available, not null, we use setTimeout to ensure that the focus on the email input element happens asynchronously:
useEffect(() => {
if (isOpen && focusInputRef.current) {
setTimeout(() => {
focusInputRef.current!.focus();
}, 0);
}
}, [isOpen]);
This allows the modal to be fully rendered before focusing on the input.
The function handleInputChange is responsible for handling changes in the two form input fields — the user’s email address and newsletter frequency preferences. This function is triggered by the onChange event of the email input and frequency select elements:
const handleInputChange = (
event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
const { name, value } = event.target;
setFormState((prevFormData) => ({
...prevFormData,
[name]: value,
}));
};
When called, the function extracts the name and value from the event’s target — in other words, the form element that triggered the change. It then uses the setFormState state variable to update the form state.
Additionally, the handleInputChange function uses the callback form of setFormState to correctly update the state. This preserves the previous form data using the spread operator — ...prevFormData — and updates only the changed field:
The function handleSubmit is called when the form is submitted. It is triggered by the onSubmit event of the form:
const handleSubmit = (event: React.FormEvent): void => {
event.preventDefault();
onSubmit(formState);
};
If the close button in the Modal is clicked, we should roll back to the previous data (modalData, in this case) and also execute the onClose() callback:
const handleClose = () => {
setFormData(modalData);
onClose();
};
This function prevents the default form submission behavior using event.preventDefault() to avoid a page reload. Then, it calls the onSubmit function from props, passing the current formState as an argument to submit the form data to the parent component.
After submission, it resets the formState to initialNewsletterModalData, effectively clearing the form inputs.
Modal componentIn the JSX block, we return the Modal component, which will be rendered with the modal’s content.
We use our custom Modal component and pass it three props — hasCloseBtn, isOpen, and onClose. The form elements — inputs, labels, and submit button — will be rendered within the Modal component:
return (
<Modal
hasCloseButton={true}
isOpen={isOpen}
onClose={handleClose}
>
{/* Form JSX goes here... */}
</Modal>
);
Inside the Modal component, we render a form element containing two sections with labels and form elements corresponding to the input field and select dropdown. The input field is for the user’s email, and the select dropdown allows the user to choose the newsletter frequency.
We bind these elements with the onChange event handler to update the formState when the user interacts with the form. The form element has an onSubmit event that triggers the handleSubmit function when the user submits the form:
<form onSubmit={handleSubmit}>
<div className="form-row">
<label htmlFor="email">Email</label>
<input
ref={focusInputRef}
type="email"
id="email"
name="email"
value={formState.email}
onChange={handleInputChange}
required
/>
</div>
<div className="form-row">
<label htmlFor="digestType">Digest Type</label>
<select
id="digestType"
name="digestType"
value={formState.digestType}
onChange={handleInputChange}
required
>
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
<option value="monthly">Monthly</option>
</select>
</div>
<div className="form-row">
<button type="submit">Submit</button>
</div>
</form>
And this concludes our NewsletterModal component. We can now export it as a default module and move on to the next section, where we will use it and finally see our Modal component in action.
NewsletterModalIn our App.tsx file — or any parent component of your choice — let’s begin by importing the necessary dependencies such as React, useState, NewsletterModal, and NewsletterModalData. If desired, we can also use the App.css or the related component stylesheet to style this parent component:
import React, { useState } from 'react';
import NewsletterModal, { NewsletterModalData } from './components/NewsletterModal/NewsletterModal';
import './App.css';
As discussed earlier, NewsletterModalData is an interface that defines the shape of the data to be passed between components to support the data within our NewsletterModal component.
Within the App component, we utilize the useState Hook to establish two state variables:
isNewsletterModalOpen: A Boolean state variable that tracks whether the newsletter modal is open or not. It is initialized as false, meaning the modal is initially closednewsletterFormData: A state object of type NewsletterModalData that holds the form data submitted through the NewsletterModal. It is initialized as null because no data is available initiallyHere’s how the code should look:
const App = () => {
const [isNewsletterModalOpen, setNewsletterModalOpen] =
useState<boolean>(false);
// Example default data (could be fetched from an API)
const defaultNewsletterModalData: NewsletterModalData = {
email: "",
digestType: "weekly",
};
const [newsletterFormData, setNewsletterFormData] =
useState<NewsletterModalData>(defaultNewsletterModalData);
// Rest of the component implementation goes here...
};
To handle the modal states, we define two functions: handleOpenNewsletterModal and handleCloseNewsletterModal. These functions are used to control the state of the isNewsletterModalOpen variable.
When handleOpenNewsletterModal is called, it sets isNewsletterModalOpen to true, opening the newsletter modal. When handleCloseNewsletterModal is called, it sets isNewsletterModalOpen to false, closing the newsletter modal.
See the code below:
const handleOpenNewsletterModal = () => {
setNewsletterModalOpen(true);
};
const handleCloseNewsletterModal = () => {
setNewsletterModalOpen(false);
};
The handleSubmit function is called when the user submits the form inside the NewsletterModal. It takes the form data from the NewsletterModalData interface as an argument.
When called, the handleSubmit function sets the newsletterFormData state variable to the submitted data. After setting the data, it calls handleCloseNewsletterModal to close the modal:
const handleFormSubmit = (data: NewsletterModalData): void => {
setNewsletterFormData(data);
handleCloseNewsletterModal();
};
Finally, we return the JSX that will be displayed as the UI for the App component.
In the JSX, we have a div containing a button. When clicked, this button triggers the handleOpenNewsletterModal function, thereby opening the newsletter modal.
We check if newsletterFormData is not null and if its email property is truthy. If both conditions are met, we render a message using the data from the newsletterFormData.
Then, we render the NewsletterModal component, passing the necessary props — isOpen, onSubmit, and onClose. These props are set as follows:
isOpen — set to the value of isNewsletterModalOpen to determine whether the modal should be displayed or notonSubmit — set to the handleSubmit function to handle form submissionsonClose — set to the handleCloseNewsletterModal function to close the modal when requestedSee the code below:
return (
<>
<div style={{ display: "flex", gap: "1em" }}>
<button onClick={handleOpenNewsletterModal}>Open the Newsletter Modal</button>
</div>
{newsletterFormData && newsletterFormData.email && (
<div className="msg-box msg-box--success">
<b>{newsletterFormData.email}</b> requested a <b>{newsletterFormData.frequency}</b> newsletter subscription.
</div>
)}
<NewsletterModal
isOpen={isNewsletterModalOpen}
onSubmit={handleFormSubmit}
onClose={handleCloseNewsletterModal}
/>
</>
);
That’s it! We now have our App component up and running, showing a button to open a functional newsletter modal. When the user submits the form with the appropriate information, that data is displayed on the main app page, and the modal is closed.
Check out the below given CodePen demo below showcasing the implementation of all the code snippets mentioned earlier:
See the Pen
React Modal Component with HTML5 Dialog API by Rahul (@_rahul)
on CodePen.
For a well-organized and comprehensive version of this project, you can access the complete code on GitHub. Please note that this implementation is written in TypeScript, but it can be adapted to JavaScript by removing the type annotations as I did in this StackBlitz demo.
Nowadays, methods for creating modal dialogs no longer rely on third-party libraries. Instead, we can utilize the widely supported native <dialog> element to enhance our UI modal components. This article provided a detailed explanation for creating such a modal component in React, which can be further extended and customized to suit the specific requirements of your project.
If you have any questions, leave them in the comment section below!
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>

line-clamp to trim lines of textMaster the CSS line-clamp property. Learn how to truncate text lines, ensure cross-browser compatibility, and avoid hidden UX pitfalls when designing modern web layouts.

Discover seven custom React Hooks that will simplify your web development process and make you a faster, better, more efficient developer.

Promise.all still relevant in 2025?In 2025, async JavaScript looks very different. With tools like Promise.any, Promise.allSettled, and Array.fromAsync, many developers wonder if Promise.all is still worth it. The short answer is yes — but only if you know when and why to use it.

Discover what’s new in The Replay, LogRocket’s newsletter for dev and engineering leaders, in the October 29th issue.
Hey there, want to help make our blog better?
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