Written by Rahul Chhodde✏️
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. Starting from scratch will allow us to explore the process in detail, including:
- What is a modal dialog?
- What is a non-modal dialog?
- Understanding the native HTML
<dialog>
element - How to construct a modal using the
HTMLDialogElement
API - Styling a modal powered by the HTML
<dialog>
element - Creating a pop-up modal in React using the
<dialog>
element - Using our React
Modal
component - Implementing the
NewsletterModal
You can check out the complete code for our React pop-up modal in this GitHub repository.
What is a modal dialog?
A modal dialog is a UI element that temporarily blocks the 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.
What is a non-modal dialog?
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. Instead of expanding further on non-modals, we will maintain our emphasis on creating and discussing modal dialogs.
Understanding the native HTML <dialog>
element
Before the native HTML <dialog>
element was introduced, developers had to rely solely on JavaScript to add the required HTML to the document to obtain the modal functionality.
However, the native HTML <dialog>
element is now widely supported on modern browsers. Thanks to the JavaScript API specifically designed for the <dialog>
element, modal dialogs have become more semantically coherent and easier to handle.
This also means that you no longer require a third-party library to construct your own pop-up modals.
How to construct a modal using the HTMLDialogElement
API
The markup required to structure a native <dialog>
element is quite straightforward. Let'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 dialog = document.getElementById('myDialog');
const openDialogButton = document.getElementById('openDialog');
const closeDialogButton = document.getElementById('closeDialog');
openDialogButton.addEventListener('click', () => {
dialog.showModal();
});
closeDialogButton.addEventListener('click', () => {
dialog.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 and lacking in style, but it is fully functional. Moreover, it is much easier to integrate and provides greater semantic value compared to a comprehensive modal solution built entirely with JavaScript:
See the Pen Simple modal example by Rahul C (@_rahul) on CodePen.
Styling a modal powered by the HTML <dialog>
element
A 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.
The :modal
pseudo-class
The :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. Further, 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;
}
The ::backdrop
pseudo-element
When 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 in action:
See the Pen Custom styled modal demo by Rahul C (@_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.
Creating a pop-up modal in React using the <dialog>
element
Now 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 sub-directory 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 React from "react";
const Modal: React.FC = () => {
const modalRef = useRef<HTMLDialogElement | null>(null);
return (
<dialog ref={modalRef} className="modal">
{children}
</dialog>
);
}
export default Modal;
In the above code snippet, we define the Modal
component using the React functional component syntax. We use the useRef
Hook to create a reference to the HTML <dialog>
element that we could use later on in the useEffect
Hooks.
To make this component work, we need to consider the following points to determine the props we will need:
- Check open/closed state: We need to keep track of the state of the dialog component — whether it is open or closed. We can use another Boolean prop for this purpose
- Decide on a close button: We need to decide whether or not the dialog component should include a close button. This can also be controlled through a Boolean prop
- Define closing behavior: We need to specify the desired behavior when the dialog is closed. This includes determining what actions or events should be triggered upon closing, which can be accomplished with a callback function as a prop
- Handle children appropriately: We need to enable this component to accept other HTML nodes as children. This can be achieved by utilizing the special
props.children
prop provided by React
The 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;
};
After planning the props for the Modal
component, it is ideal to define a state variable to manage its opening and closing states:
const Modal: React.FC<MOdalProps> = ({ isOpen, hasCloseBtn, onClose, children }) => {
const [isModalOpen, setModalOpen] = useState(isOpen);
const modalRef = useRef<HTMLDialogElement | null>(null);
return (
<dialog ref={modalRef} className="modal">
{children}
</dialog>
);
};
With the basic structure of our Modal
component in place, we can now proceed to implement the functionality for opening the modal.
Opening the 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.
In the case of opening our modal, we can use a useEffect
Hook that gets triggered whenever the isOpen
prop changes. This ensures that the isModalOpen
state stays in sync with the isOpen
prop, allowing the component to respond accurately to external changes while maintaining consistency between the two:
useEffect(() => {
setModalOpen(isOpen);
}, [isOpen]);
Note that we are not going to take advantage of conditionally rendering elements in the DOM based on React state variables. Instead, we will use the HTMLDialogElement
API to manage the visibility of our Modal
dialog.
We used this same approach in the plain HTML and JavaScript example we explored above. This approach will also allow us to make the most of the built-in accessibility features provided by the native HTML <dialog>
element.
To implement the HTML <dialog>
modal with React, we will utilize the isModalOpen
state variable in conjunction with another useEffect
Hook. This Hook will control the modal’s visibility by calling Dialog.showModal()
when isModalOpen
is true, effectively displaying the modal.
Conversely, when isModalOpen
is false, it will invoke Dialog.close()
to hide the modal. This way, the modal’s display state will always be in sync with the value of the isModalOpen
state variable. See the code below:
useEffect(() => {
const modalElement = modalRef.current;
if (modalElement) {
if (isModalOpen) {
modalElement.showModal();
} else {
modalElement.close();
}
}
}, [isModalOpen]);
Closing the Modal
We can create a utility function that incorporates the optional onClose
callback and set isModalOpen
to false
. This function can be used later to easily close the Modal
dialog in different scenarios:
const handleCloseModal = () => {
if (onClose) {
onClose();
}
setModalOpen(false);
};
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, since we are managing the states of our Modal
component using the useState
Hook, we need to update it accordingly when the escape key is pressed to ensure the proper functioning of the Modal
dialog.
To achieve this, we can simply listen for a KeyDown
event and call the handleCloseModal
function, which we declared earlier, whenever the event 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 and prevents any conflicts between the HTMLDialogElement
API and React states.
Piecing it all together
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.
Additionally, we will 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>
);
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.
Using our React Modal
component
Now, let's put the modal dialog component to use and observe its functionality.
For this purpose, we’ll consider one of the commonly seen modal UI elements on the web: the typical 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.
Additionally, we will demonstrate how to gather and manage data in the frontend using this method. Moreover, the same data can be seamlessly transferred to either the frontend or the API as required.
Setting up the NewsletterModal
component
The 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';
Defining types and props
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 will once again utilize TypeScript interfaces. We'll also export this interface to reuse it in the main App
component:
export interface NewsletterModalData {
email: string;
frequency: string;
}
Based on the provided type definition, we can now set the default or initial data that our newsletter form should hold. We'll use an object to manage the data for the email and frequency fields:
const initialNewsletterModalData: NewsletterModalData = {
email: '',
frequency: 'weekly',
};
Next, we'll define the props that our NewsletterModal
component will receive:
interface NewsletterModalProps {
isOpen: boolean;
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 not -
onSubmit
— A function that will be called when the form is submitted. It takes a property of theNewsletterModalData
type as an argument -
onClose
— A function that will be called when the user closes the modal
Two of these props, namely isOpen
and onClose
, will further be used as prop values for the Modal
component.
Defining the NewsletterModal
component
Now, 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: React.FC<NewsletterModalProps> = ({
onSubmit,
isOpen,
onClose,
}) => {
// Component implementation goes here...
};
Managing states and references
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>(null);
const [formState, setFormState] = useState<NewsletterModalData>(
initialNewsletterModalData
);
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.
Handling data 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>
): void => {
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:
Handling form submission
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);
setFormState(initialNewsletterModalData);
};
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.
Consuming the Modal
component
In 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={onClose}
>
{/* 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.
Implementing the 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 asfalse
, meaning the modal is initially closed -
newsletterFormData
— A state variable that holds the form data submitted through theNewsletterModal
. It is initialized asnull
since no data is available initially
Here’s how the code should look:
const App: React.FC = () => {
const [isNewsletterModalOpen, setNewsletterModalOpen] = useState<boolean>(false);
const [newsletterFormData, setNewsletterFormData] = useState<NewsletterModalData | null>(null);
// 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:
e>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 ofisNewsletterModalOpen
to determine whether the modal should be displayed or not -
onSubmit
— set to thehandleSubmit
function to handle form submissions -
onClose
— set to thehandleCloseNewsletterModal
function to close the modal when requested
See 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 showcasing the implementation of all the code snippets mentioned earlier:
See the Pen Newsletter form modal demo by Rahul C (@_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 here.
Conclusion
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.
The article provided a detailed explanation of 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, feel free to let me know.
Get set up with LogRocket's modern React error tracking in minutes:
- Visit https://logrocket.com/signup/ to get an app ID.
- Install LogRocket via NPM or script tag.
LogRocket.init()
must be called client-side, not server-side.
NPM:
$ npm i --save logrocket
// Code:
import LogRocket from 'logrocket';
LogRocket.init('app/id');
Script Tag:
Add to your HTML:
<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>
3.(Optional) Install plugins for deeper integrations with your stack:
- Redux middleware
- ngrx middleware
- Vuex plugin
Top comments (0)