Mayank

Resetting default dialog styles

Since I’ve touched on resetting popover styles, I thought I’d do the same for <dialog>. They kinda go hand-in-hand.

The HTML standard requires the following user-agent styles for <dialog>:

Code snippet
dialog:not([open]) {
  display: none;
}
dialog {
  position: absolute;
  inset-inline-start: 0;
  inset-inline-end: 0;
  width: fit-content;
  height: fit-content;
  margin: auto;
  border: solid;
  padding: 1em;
  background-color: Canvas;
  color: CanvasText;
}
dialog:modal {
  position: fixed;
  overflow: auto;
  inset-block: 0;
  max-width: calc(100% - 6px - 2em);
  max-height: calc(100% - 6px - 2em);
}

Some of this makes sense, especially since it’s its own dedicated element (unlike the popover attribute). But then there are other parts that might make you question your faith in CSS. max-width: calc(100% - 6px - 2em) looks like something plucked from a legacy codebase. I guess the <dialog> element is quite old (first shipping in Chrome 37), so “legacy” is not inaccurate.

It’s not that there is no reason behind it; 2em is just twice the 1em padding and the 6px is twice the default border width (given by border: solid). Since all elements default to box-sizing: content-box, the padding and border values need to be subtracted from the max size in order to prevent the dialog from overflowing off screen. It’s a guardrail designed with the user in mind.

But guess what: almost every website in the world uses box-sizing: border-box. Suddenly these “guardrails” feel more like bad historical decisions. I really hope we can avoid such frustrating situations for any future new HTML elements. (Just make them default to border-box, it will be okay.)

Anyway, let’s look at how we can fix this in author-land using a CSS reset.

More sensible defaults

Let’s start with everyone’s favorite:

Code snippet
*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

Now, you might be tempted to do something like dialog { all: unset }. Don’t. It would get rid of some desirable styles, like position: fixed and (weirdly) user-select: text.

Here’s what I use in my CSS reset:

Code snippet
dialog {
  border: none;
  background: none;
  color: inherit;
  inset: unset;
  max-width: unset;
  max-height: unset;
}
dialog:not([open], [popover]) {
  display: none !important;
}

This removes only the undesirable UA styles, while reinforcing the closed state and also supporting <dialog popover>.

Since this is so similar to the popover reset, I actually decided to combine them! I also use :where to create a zero-specificity selectors1 with forgiving selector lists2.

Here’s the combined reset:

Code snippet
:where(dialog, [popover]) {
  border: none;
  background: none;
  color: inherit;
  inset: unset;
  max-width: unset;
  max-height: unset;
}
:where(dialog:not([open], [popover]), [popover]:not(:popover-open)) {
  display: none !important;
}

Bonus

I also like to add this to prevent the page from being scrollable when a modal dialog is open:

Code snippet
:where(html:has(dialog:modal[open])) {
  overflow: clip;
}

Combine it with scrollbar-gutter: stable to prevent layout shift.

Code snippet
:where(html) {
  scrollbar-gutter: stable;
}

Footnotes

  1. The zero specificity can be useful because, being a reset, this should have the lowest priority. This is mostly for other people using my reset. Personally, I like to use cascade layers instead, avoiding any cascade issues altogether.

  2. The forgiving selector list is useful because we’re using new CSS selectors like :popover-open that could break the whole selector in older browsers.