A “modal” is a pop up that a site can use to display more information or interactivity. Usually, if we need one, we build it ourselves. But that can be tricky; the biggest pain points about modals are making sure to show/close them at the right times, the CSS, and blocking interactions from happening outside of it. And they must be accessible of course!
But, did you know that HTML has a tag to make modals for you? The dialog tag is a super handy element that does everything we need, it's got a bunch of features and accessibility controls built right in for us. I'm going to explain everything in vanilla JS/HTML/CSS and then show how to seamlessly incorporate it into any framework like React at the end.
The basics of making a modal
Here's our starting code:
It's so few lines of JS I'm just going to use a script tag to keep it all in once place. All we need is a button to open our modal for now. Next, we need the dialog
tag. By the way, it's called dialog because its a window we can use to tell our users them something. Anyway, let's add the modal right below our button:
Ok now if you're following along…nothing happened. That's because, by default, a dialog is display: none
. We need some way to show it. Don't worry, it couldn't be easier: use the new showModal()
method whenever we click the button:
That's it! You just grab the modal
element, and then call .showModal()
. Note that I just added an id
to our dialog
tag to make the link between "modal" and "dialog" super clear.
Closing our modal
This is honestly one of the neatest parts. Power users know that hitting the escape key is usually how you can exit menus and things. Well guess what? That functionality comes built in to dialogs
. Nothing you need to do at all! This is the power of web standards! But, obviously we do want some way to close our model with a button.
So, here's another cool thing about dialog
elements: we don't need any JS to close them. If you include a form
in a modal and set the method attribute to “dialog” it will automatically close the dialog
for you! That aria-label
attribute on the button is required for screen readers to work properly, make sure you include it!
To clarify, any form
inside a dialog
with a method of dialog
will not GET
or POST
, but will instead have its default behavior be to close the dialog. There is a JS way to close forms as well, modal.close()
which we'll cover later on when we use more in depth forms, so don't panic!
The point is, we can do a lot of stuff with dialogs
before we need to bother much with JS.
The open
attribute
And before we move on, you should know there's an open
attribute that will be added onto our dialog tag every time we open it (it'll look like: <dialog open>
). It's an attribute that has no value, it just exists. You can edit this yourself, but that will actually trigger a slightly different behavior. This article teaches the showModal()
logic instead. I (and MDN) don't recommend manipulating the open
property directly, I'm only bringing it up because it can be useful for CSS stuff.
Speaking of styling...
Understanding and styling the backdrop
Now that we have a functional modal, let's take a second to talk about the overlay behind it that covers our page. It's officially called the "backdrop", and it's what allows us to block interaction with our page while our modal is open. So if a user hits tab or tries to click any link or button outside, it won't work. Your keyboard focus should be locked, and every other element on the page will be in an inert
state.
Go ahead, add this interaction button to our site and see that you can't tab over to it or click on it when the modal is open:
Backdrop styles
By default, the backdrop pseudo element has a really simple, transparent, gray style. But we can alter this to be anything we want. Me personally, I like to add a blur and darker color:
That should give a really nice look to it. Now, by default you can still scroll under a modal. Again, personally this doesn't bother me. You can control this by using classes added to the body with JS or you can use the new has
function in CSS to do this super cool one liner that takes advantage of the open attribute that only appears when the backdrop is visible:
Clicking the backdrop to fire modal.close
Lastly, a behavior that a lot of people are used to but does not come standard with this modal is the ability to close the it by clicking the backdrop. To accomplish this, you just need to add a simple function:
We're adding a click listener to our modal and looking at the coordinates of the event. Which, even if you click the backdrop pseudo element, will still only give the coordinates of the actual modal box itself. Then we figure out where the mouse clicked, and if it's outside of the box, we close the modal. We close by calling the close
method on our modal element. Oh and that little check for 0 on x
and y
handles certain inputs triggering click events when a user uses a keyboard to select them.
It's a cool little function, I'll leave it out of the next examples to keep things clean for this tutorial, but I recommend adding it to your modals.
Using REAL forms in our modal
It's true that we have a small form for our “X” button, but It's a super common use case to have a modal pop up with a real form inside of it to get some information. So let's make one ourselves!
Simple form with the close
event
Admittedly, I've never bothered with this technique. However, if you just want a super simple form with a few options, there's a neat way you can handle it: the close
event. Unlike traditional forms, when you use method="dialog"
there's a returnValue
that you can access on the close
event, which is of course fired whenever we close a modal.
Now, when they click "yes" or "no" that button's value gets set to the close event's e.target.returnValue
. That value can only be set by a submit button, and it must be a string. So, using a few buttons and a close event listener is a perfect way to get simple answers. You could also tell if the “X” button was clicked as well, since we didn't give that button an explicit value.
Note: the close
event is not cancelable and doesn't bubble, so no event delegation here, you have to attach the listener directly to the modal.
And despite e.target.returnValue
coming from the button
, the target
is the dialog
itself.
Traditional form submission event
If you need to collect some real data in the form, I recommend sticking with the submit
event. Here's the only weird thing: don't e.preventDefault
, use the method="dialog"
. That method
will make the default behavior into closing the modal, which is what we want! Don't forget to reset the form though (unless you want to save answers, I guess).
Notice that still we have our first form with the close button, that's fine! No rule about the number of forms in a modal. Also, I took out the close
event because I don't need it, but submit
events happen first. Not having that e.preventDefault()
feels so freeing, and I can't really explain why.
A cancel button
Sometimes you'll see modal forms with a “cancel” button next to submit. Just use a non-submitting button, add a click listener to it, and then close the modal on click:
And that's it! There are a few little minor details and extra features you can read about in the dialog docs, but this is like 99% of all you'll need to know about modals and forms!
React example
The best thing about web standards is how standard they are. That means they super easily integrate with frameworks. React is the one I know so lets show that, but seriously any web framework can use dialog
because any browser can.
For React, the biggest thing is that you need to use the useRef
hook. Other than that, everything is the same. You can of course control your forms, but I'm not going to bother here so it's super clear everything is the same. Of course, if this is new to you, check out this article about non-controlled forms.
And there you have it! See how easily it incorporates into React? There's even an onClose
event and everything. So, there's really no reason not to start using web standard modals on your next project!
Happy coding everyone,
Mike