Wasn’t it nice — nay, wonderful – the day we bought a local HTML component? It’s, what now, almost ten years previous? Even so, I at all times discover myself trying up the way it works, when to make use of it, concepts for styling it, and whatnot. There’s numerous nuance to this seemingly little piece of net structure and it’s excessive time I give it a correct look… for future Geoff, however possibly future you as effectively.
Marking up a
That is the essential markup:
It gained’t open by default. We may manually set the open attribute:
However when do you ever need to open a dialing by default? I’m positive it’s a uncommon use case. As a substitute, we’ve got a JavaScript open() methodology for that. We will arrange variables for the dialog and button, then invoke the tactic:
const dialogButton = doc.querySelector('#dialog-button');
const dialog = doc.querySelect('#dialog');
formButton.addEventListener('click on', () => {
dialog.present();
})
That works, however purchaser beware — that treats the dialog as extra of a pop-up than a modal, and modal is what I feel you’ll need most often. The important thing variations? A modal features a backdrop, is mechanically positioned within the middle of the web page, and permits the Esc key to shut it.
Discover how present() lacks the backdrop, positioning, and shutting stuff:
So, possibly invoke the showModal() methodology as a substitute:
const dialogButton = doc.querySelector('#dialog-button');
const formDialog = doc.querySelector('#dialog');
formButton.addEventListener('click on', () => {
formDialog.showModal();
})
Nice! It opens, positions… and closes:
Extra about closing
Now we are able to hit the Esc key when the dialog is in focus (and see the way it’s in focus by default) to shut the dialog. But when we would like some UI that closes the dialog, like a button, we are able to add that contained in the component:
That doesn’t work proper out of the field however I wager you’ve already guessed how. There’s a shut() methodology for it:
const formButton = doc.querySelector('#dialog-button');
const formDialog = doc.querySelector('#dialog');
const formClose = doc.querySelector('#dialog-close');
formButton.addEventListener('click on', () => {
formDialog.showModal();
})
formClose.addEventListener('click on', () => {
formDialog.shut();
})
I feel it’s a little bit humorous/bizarre that there isn’t a corresponding closeModal(). No worries, although, as a result of shut() simply works:
And if you’d like a JavasScript-less strategy, we are able to really do it declaratively instantly within the HTML:
I can’t vouch for the way that impacts semantics, however it’s certainly doable to shut that sucker with out JavaScript.
Invoker Instructions
So long as we’re speaking about declarative closing, we could as effectively point out an evolving function known as invoker instructions which can be designed particularly to open and shut dialogs declaratively. It’s all completely experimental as I write this, however test this out:
That’s proper! We will hook up the button to the dialog instantly in HTML with the command and and commandfor attributes to invoke a particular dialog (#my-dialog) to show-modal.
Cool, and that’s true for a closing button as effectively:
And, as Danny explains right here, we are able to hook all that up with JavasScript if we have to take heed to these instructions and fireplace off some occasion once they occur:
// Choose all dialogs
const dialogs = doc.querySelectorAll("dialog");
// Loop all dialogs
dialogs.forEach(dialog => {
// Pay attention for shut (as regular)
dialog.addEventListener("shut", () => {
// Dialog was closed
});
// Pay attention for command
dialog.addEventListener("command", occasion => {
// If command is show-modal
if (occasion.command == "show-modal") {
// Dialog was proven (modally)
}
// One other approach to hear for shut
else if (occasion.command == "shut") {
// Dialog was closed
}
});
});
Carry on eye on that assist!
About button labeling
You is perhaps tempted to make use of an “X” (or not less than an SVG icon for it) for the shut button’s label:
…however that’s not precisely the perfect factor for screenreaders to announce. We nonetheless need it to say “Shut Dialog” or one thing to that impact. So, in case you’re eager on utilizing an “X” icon, I’d add a containing the textual content we would like learn and visually disguise it whereas stopping the icon from being introduced utilizing the aria-hidden attribute:
Another accessibility-minded word. See how the shut button will get focus when the dialog opens up?
You could or could not need that as a result of now the button may unexpectedly shut the dialog if the House key’s by accident hit. Not the tip of the world as a result of closing a modal isn’t precisely a harmful factor and may be opened again up. However if in case you have different focusable components within the dialog — maybe a hyperlink or a type area — then possibly contemplate giving a kind of preliminary focus with the tabindex attribute.
Innate inertness
I need to get into styling the backdrop, however earlier than that, I feel it’s value noting that the web page behind an open dialog is inert. In different phrases, any kind of interplay — textual content choice, button clicking, focus, inputs, and many others. — are unavailable. That’s in all probability what you need anyway, so it’s good that isn’t one thing that needs to be configured by default. You simply gained’t really see the inert attribute within the markup when it occurs.
However that’s solely when the dialog is setup as a mannequin. Keep in mind the very first demo? We used the present() methodology to open the dialog on click on relatively than the extra express showModal(). Meaning the primary demo is exhibiting off one thing extra like a popover (suppose tooltips) than a real attention-hoarding modal that traps focus and sits on the very prime layer.
You may surprise about competing dialogs, like say an a popover and modal which can be open on the identical time. Properly, how did you open them on the identical time within the first place? You’d should open the dialog popover first since that doesn’t set off inert conduct. Solely the modal dialog does. And when the modal dialog is open, the popover dialog shouldn’t be within the prime layer, making it inaccessible.
Anyway, let’s get to styling!
Styling the backdrop
Let’s begin right here as a result of I feel it’s extremely arduous to even see the backdrop the way in which it’s styled by default. For those who open dialog within the final instance, discover that the web page background is barely tinted. Not a lot, although. That’s the backdrop.

Very refined. We will type that ourselves with the ::backdrop pseudo-element. For instance, we may go full-on stable coloration:
Buuuuut now we’re obscuring all the web page behind it. That is perhaps OK, however I additionally suppose a little bit transparency, maybe with a little bit blur() motion can’t damage as a result of, you realize, context:
I really actually like Mojtaba’s background picture instance within the CSS-Tips Almanac, even when it contradicts my emotions about obscuring the remainder of the web page:
Styling the border and background
Two very apparent defaults are setting the ‘s styling: a vanilla white background and massive ol’ black border. Completely superb to go away that as-is in case you’d like. Or, not.

You may suppose your customized kinds would go proper on the component:
/* 👎 */
dialog {
background-color: gold;
border: 0;
border-radius: 12px;
}
However you really need to choose it in its open state:
particulars {
/* ... */
&:open {
background-color: gold;
border: 0;
border-radius: 12px;
}
}
You could have observed within the DevTools screenshot up there that the :modal pseudo-class has even increased specificity than :open. You possibly can completely use that as effectively must you want overrides to the overrides.
Be careful for that :open pseudo-class, although. Safari 26.5 simply gained assist for it the day I’m penning this. For those who want deeper assist, contemplate deciding on the [open] attribute as a substitute… or simply utilizing :modal.
Styling the place
A much less apparent default dialog type is the way it’s positioned within the middle of the viewport. Open DevTools and also you’ll see the UA styling that does that:

We may override margin-top to make the dialog a little bit extra cosy with the highest of the viewport:
One factor you in all probability don’t need to do is override your dialog’s show. It’s set to show: none in its preliminary closed state. Set that to one thing like block on the component itself and also you completely lose the entire level of getting a modal — the entire closed by default factor. You continue to get primary opening and shutting, solely with out the helpful Esc key affordance.
And spot how the customized kinds are solely utilized on the :open state since that’s the place they dwell:
Likelihood is that you simply don’t need the content material behind the ::backdrop to scroll. It’s a kind of conditions the place a person may be taken out of context and positioned someplace completely completely different on the web page than the place they had been when opening the dialog.
It’d be very nice if the underlying content material was caught in place by default, however it’s completely comprehensible why it doesn’t: a dialog shouldn’t be a scroll container. If it was, we may slap overscroll-behavior: comprise on it and be achieved with it.
Properly, seems that Chrome 144 tweaked that up a bit in order that overscroll-behavior works on non-scrollable scroll containers. So, assuming we’re in Chrome 144+, we are able to set that conduct on the dialog and its backdrop:
dialog {
overscroll-behavior: comprise;
&::backdrop {
overscroll-behavior: comprise;
}
}
The final lacking piece is that we have to make the dialog itself a scroll container:
dialog {
overflow: hidden;
overscroll-behavior: comprise;
&::backdrop {
overscroll-behavior: comprise;
}
}
Chrome 144 or above wanted:
That’s cool and all, however one other (and extra concise) approach to do it with broad browser assist is to test if the physique component :has() a dialog with an open attribute. And if it does, we disguise the physique overflow:
physique:has(dialog[open]) {
overflow: hidden
}
That mentioned, I do just like the overscroll-behavior strategy as a result of it’s extra declarative and connected to the component we’re deciding on.
Getting artistic with dialog styling
Andy Clarke has a whole write-up on artistic methods to type dialogs past the essential content-in-box. It’s effectively out of scope of what we’re masking right here, however effectively well worth the learn. Right here’s one instance to whet your urge for food:
Animating dialogs
Dialogs kinda “snap” out and in once they’re opened and closed. However we are able to sprinkle in a little bit animation for once they enter and exit view.
Like, what if we made the dialog slowly fade in as a substitute. You may suppose this could work:
/* Nope! 👎 */
dialog {
opacity: 0;
overflow: hidden;
overscroll-behavior: comprise;
transition: opacity .5s ease-in-out;
width: 80vw;
&:open {
opacity: 1;
}
}
However, no. We have now to explicitly set a beginning type for components simply as they’re rendered within the DOM. On this case, a dialog is show: none by default and has no opacity set on it when it’s activated. That’s the place the @starting-style at-rule comes into play:
/* Yep! 👍 */
@starting-style {
dialog:open {
opacity: 0;
}
}
dialog {
overflow: hidden;
overscroll-behavior: comprise;
transition: opacity .5s ease-in-out;
width: 80vw;
&:open {
opacity: 1;
}
}
There we go:
Coming into and exiting view? That really feels like prime View Transitions API territory! However, alas, dialogs usually are not an excellent use case for them. Why? Modal dialogs dwell within the prime layer, and shutting them can take away them in a method that doesn’t at all times produce a dependable previous/new pair for the named transition.
Right here’s an instance of that the place we’ve got a view-transition-name set on the dialog component after which the ::view-transition-new() and ::view-transition-old() states certain to that identify, every calling an animation that slides in and slides out, respectively. Works effectively for the beginning transition, however not a lot for the exiting transition. Discover, too, that the backdrop wants further work because it’s included within the combine:
What you are able to do as a substitute is a few kind of hybrid strategy by setting the view transition on the open state and utilizing a CSS animation on the shut state.
Or possibly simply use CSS animations/transitions for each states! I’m undecided there’s any actual added worth in utilizing a view transition on one state for the sake of utilizing a view transition.
Anyway, in case you’re seeking to get extra artistic with in-n-out animations, Chris Coyier has a fairly cool one the place the modal follows a form() path. His demonstrates a dialog configured as a popover, so I forked it and used a modal as a substitute:
Dialog or Popover?
Which one must you use? It’s a very good query as a result of the Dialog API and Popover API are tremendous comparable however designed for various use instances. Zell Liew has a concise reply:
After
a bita number of analysis, I found that the Popover API and Dialog API are wildly completely different when it comes to accessibility. So, in case you’re attempting to determine whether or not to make use of Popover API or Dialog’s API, I like to recommend you:
- Use Popover API for many popovers.
- Use Dialog’s API just for modal dialogs.
The “when it comes to accessibility” is what actually issues right here as a result of, popovers lack:
- automated focus administration,
- automated ARIA connection, and
- automated gentle dismiss
In the meantime, a dialog:
- mechanically
inerts different components, - prevents customers from tabbing into different components, and
- prevents display readers from reaching different components.
So, in case you’re planning to make use of a popover and want accessible affordances for trapping focus and making different components inert, you’ll must deal with these by yourself in JavaScript.
Zell additionally notes that popovers want an express accessible function. And there are a number of to select from so it’s gonna take some thought to decide on the best one.
This isn’t all to say, hey, at all times use a dialog. It’s extra about choosing the proper API for the best use case. Quoting Zell once more:
- Popover is an umbrella time period for any sort of on-demand popup.
- Dialog is one sort of popover — a sort that creates a brand new window (or card) to comprise some content material.
Wrapping up
That’s all for now. I’ll replace this if y’all have extra so as to add or higher or extra correct methods to articulate what’s right here. Future us-es (there’s no plural for us, proper?) will thank us later.
