The <dialog> element is used to create a dialog box or other interactive component in a web page. It can be opened and closed programmatically using JavaScript. This semantic element is a great replacement for the many div elements that people have create over the years named something along the lines of modal.
The dialog is a native HTML element that ends up providing a shadow DOM element that provides a ::background css interface, a css layer above the rest of the document, and an API that demands only one be open at a time. All of these features would have been time consuming to do manually, and would have created a different implmentation each time.
So why then is this document file named popover if we've started out talking about dialog? Well the answer to that is that dialog was created as a good little semantic element, and then to make it, a popover API was also created, but this ended up being so much more useful in the long run that it's almost impossible to talk about the dialog without also mentioning popover.
So let's see some examples.
<dialog open>I'm poppin, ma</dialog>
Dialog elements have a default display: none, but can be turned on with an open attribute. They have a number of other default styles, but this feels like the best place to start. Since they're normally off by default, it will take some javascript to get them to be useful in practice.
There are two basic ways you can open a dialog, .show() and .showModal(). The major difference between these is a concept called inert. When you open a dialog with .showModal(), all the elements beneath the dialog will be rendered unusable. On the otherhand, opening with .show() will just open up the dialog and not particularly affect anything underneath.
<dialog id="dialog">
<button>Close</button>
<p>I'm poppin, ma</p>
</dialog>
<button>Activate Dialog</button>
<button>Activate Modal Dialog</button>
document.querySelector('#dialog + button').addEventListener('click', function() {
document.querySelector('#dialog').show();
});
document.querySelector('#dialog ~ button:nth-of-type(2)').addEventListener('click', function() {
document.querySelector('#dialog').showModal();
});
document.querySelector('#dialog button').addEventListener('click', function() {
document.querySelector('#dialog').close();
});
As you might notice some other differences occur. The show method opens the dialog inline by default, and you can still highlight text on the page while it's open. But the showModal method opens the dialog in the middle of the screen, and you are no longer able to do anything but interact with the dialog itself.
You may have also noticed that it basically looks terrible. Design is up to you, but before we go too far let me sidestep over to
The popover API is available to essentially turn any element into a dialog. The really big major difference is that none of the built in modal inertness is present in generic popovers. But to make up for that, we're given some very simple methods to activate popovers from basic html.
The way to do this is to add a popover attribute to any element element, and a matching popovertarget attribute to a button that will activate it.
<div popover id="dialog">
<button popovertarget="dialog">Close</button>
<p>I'm poppin, ma</p>
</div>
<button popovertarget="dialog">Activate Dialog</button>
What have we gained with this approach? Well notice that the outside of the popover is clickable to close it. Notice also that we didn't need any javascript. Notice that the buttons are essentially toggles, since both the inner and outer button are coded the same way.
So now that we've seen some examples of how to make a popover and a dialog, it seems like we should take a quick side step into design considerations, because genuinely if you need to know more about activation, you'll go find out about it, but for 90% of the time we've covered everything you'll need to know up to here.
So far our dialogs have looked pretty terrible. Luckily the true answer to the next question "How do I style a dialog?" is essentially "However you want". So much of the work will be already done for you, and so then it's just about aesthetics. But there are some gotchas, and some things to discuss, especially when we get to transitions and animations.
First off, we can style the dialog itself. By default, dialogs have some user agent styles applied to them, including a border, padding, and a box shadow. These will certainly be the first thing you override. Let's add some basic styles, and make the thing look pleasant. But luckily we don't have to worry about positioning at all if we don't want to. No matter what we put in there, the dialog will be center screen, which is great.
dialog {
border: none;
border-radius: 0.5em;
padding: 1em;
background-color: light-dark(#ddd, #444);
box-shadow: 0 2px 10px light-dark(gray, black);
color: light-dark(black, white);
}
The dialog element has a few pseudo elements that are useful to know about. The first is the ::backdrop pseudo element. This represents the area behind the dialog when it's open as a modal. You can style this to change the color, opacity, or even add effects like blur.
dialog {
...
&::backdrop {
background: rgb(0 0 0 / 0.5);
backdrop-filter: blur(5px);
}
}
Even though we don't have to worry about how to position our element, it is in fact worth looking at how the browser accomplishes this, as it might be using techniques you are not accustomed to.
dialog {
position: absolute;
inset-inline-start: 0px;
inset-inline-end: 0px;
width: fit-content;
height: fit-content;
background-color: canvas;
color: canvastext;
margin: auto;
border-width: initial;
border-style: solid;
border-color: initial;
border-image: initial;
padding: 1em;
}
When we look at the popover user agent styles, we get a very similar situation.
[popover] {
position: fixed;
width: fit-content;
height: fit-content;
color: canvastext;
background-color: canvas;
inset: 0px;
margin: auto;
border-width: initial;
border-style: solid;
border-color: initial;
border-image: initial;
padding: 0.25em;
overflow: auto;
}
This starts to paint the picture of the main differences between the default styles. The dialog is positioned absolutely, while the popover is positioned fixed. The popover has its entire inset set to 0, while the dialog only sets its inline to 0. It won't go to the vertical middle, just the horizontal.
The overflow being auto on the popover is interesting, since it means that the popover is prepared to scroll its content, while the dialog is not. We might even consider this for our usecases, that the popover is sort of primed for more content, while the dialog is more for short controlled bursts.
A couple things worth doing with either one would be to remove the body scroll while a popup is open, and to add the inert concept to the popover if desired. When a dialog is open, it will have a [open] attribute, and if the popover is open, it will have a :popover-open pseudo class.
html {
&:has(dialog[open], :popover-open) {
overflow: hidden; /* stop scrolling */
pointer-events: none; /* make everything inert */
user-select: none;
scrollbar-gutter: stable; /* prevent layout shift */
& :is(dialog[open], :popover-open) {
pointer-events: initial; /* re-enable pointer events for open popovers/dialogs */
user-select: initial;
}
}
}
Using scrollbar-gutter to keep the position of a scrollbar after turning off overflow is handy to stop the document from reflowing when a popup is opened. But we only want to do that when the body is actually able to be scrolled, otherwise it will add in a gutter where none is needed. This is where we can do a truly goofy trick. By using the animation-timeline:scroll() function, we can detect if the document is currently scrolled, and only then set the gutter to stable.
@keyframes detect-scroll {
from,
to {
--can-scroll: ;
}
}
html {
animation: detect-scroll linear;
animation-timeline: scroll(self);
--bg-if-can-scroll: var(--can-scroll) stable;
--bg-if-cant-scroll: auto;
&:has(dialog[open], :popover-open) {
...
scrollbar-gutter: var(--bg-if-can-scroll, var(--bg-if-cant-scroll)); /* prevent layout shift */
}
}
Designing transitions for a modal should be fairly straightforward. Just set a default value, and then transition it to another. Right? Let's try a translate.
dialog, [popover] {
transform: translateY(1rem);
transition: transform 0.3s ease;
&:is([open], :popover-open) {
transform: translateY(0);
}
}
Well that was disappointing. It just sort of popped in and out without any transition at all. The reason for this is that when a dialog or popover is closed, it is immediately removed from the render tree. This means that no transitions can occur on close, since the element is no longer there to transition.
To solve this we'll need to utilize a css transition-behavior for display and overlay called allow-discrete. This will allow an element to transition from display none to display something by allowing the element to stay displayed the entire time it's transitioning out, and immediately become displayed when transitioning in. In order for that to actually do anything, we'll also need to utilize the @starting-style at-rule.
dialog, [popover] {
translate: 0 1rem;
...
transition: translate 0.3s,
display 0.3s allow-discrete,
overlay 0.3s allow-discrete;
&:is([open], :popover-open) {
translate: 0 0;
@starting-style {
translate: 0 1rem;
}
}
}
The @starting-style should come after the transition value but on the transition state. You can also just set a transition of all 0.3s allow-discrete if you want to keep it simple.
As of the writing of this document this will work in everything except firefox. Firefox won't transition out correctly. I probably won't update this when they do, but it's worth noting now. But firefox also isn't doing a lot of things on this page, like allowing the iframes to be resizable.
Let's go ahead and add a transition to the backdrop as well. We'll fade in the opacity, and the blur. Don't forget the allow-discrete behavior.
dialog, [popover] {
translate: 0 -1rem;
opacity: 0;
transition: all 0.3s allow-discrete;
&:is([open], :popover-open) {
translate: 0 0;
opacity: 1;
@starting-style {
translate: 0 -1rem;
opacity: 0;
}
}
&::backdrop {
background-color: rgb(0 0 0 / 0.8);
opacity: 0;
backdrop-filter: blur(0px);
transition: all 0.3s allow-discrete;
}
&:is([open], :popover-open)::backdrop {
opacity: 1;
backdrop-filter: blur(5px);
@starting-style {
opacity: 0;
backdrop-filter: blur(0px);
}
}
}
Dialogs and popovers provide a great way to add user interaction that has a lot of built in functionality, design, layout, and accessibility. It is very new as of the writing of this document, so some stuff doesn't fully work in every browser. But the direction it's moving solves a lot of growing pains for application development.