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.
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.
Modal
The 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]);
Modal
We 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 Modal
onSubmit
— 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.
NewsletterModal
In 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>
Would you be interested in joining LogRocket's developer community?
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 nowJavaScript generators offer a powerful and often overlooked way to handle asynchronous operations, manage state, and process data streams.
webpack’s Module Federation allows you to easily share code and dependencies between applications, helpful in micro-frontend architecture.
Whether you’re part of the typed club or not, one function within TypeScript that can make life a lot easier is object destructuring.
Firebase is one of the most popular authentication providers available today. Meanwhile, .NET stands out as a good choice for […]