Mayank

Against the backdrop

Since I recently wrote about resetting <dialog> and popover styles styles, I guess I also have to talk about the ::backdrop pseudo-element.

I’ve been wanting to write about ::backdrop for quite some time now. I used to feel very strongly that ::backdrop is an utterly useless API. Thankfully, one of the biggest issues with ::backdrop (inheritance) was recently fixed in all browsers, which means ::backdrop is starting to become more viable than it was in the past.

The remaining issues I mention in this post are also serious problems that I would love to see solved at the platform level, but these are much easier to work around (even if the best workaround is to not use ::backdrop in the first place).

Bad default styles

The ::backdrop pseudo-element comes with the following user-agent styles (reconstructed by inspecting in browser dev-tools and reading the HTML standard):

Code snippet
::backdrop {
  display: block;
  position: fixed;
  inset: 0;
}
dialog::backdrop {
  background: rgba(0, 0, 0, 0.1);
}
:popover-open::backdrop {
  pointer-events: none !important;
  background-color: transparent;
}

I’m highlighting the problematic parts:

You can’t “click” it

When it comes to popover, you literally cannot click the ::backdrop because of pointer-events: none (as discussed above).

As for <dialog>, you can technically click the ::backdrop, meaning it intercepts clicks. But there is no backdropclick event to listen to.4 Practically, this means you need to listen to clicks on the <dialog> element, and add an extra wrapper element around all your actual content.5 But if you’re doing this, why even use the ::backdrop at all? You can make the <dialog> fill up the viewport and effectively act as a backdrop.

Or better yet, use a box-shadow that covers the entire viewport.

Code snippet
dialog:modal {
  box-shadow: 0 0 0 100vmax #0005;

  &::backdrop {
    display: none;
  }
}

And then you can listen to clicks on the document6.

Code snippet
dialog.ownerDocument.addEventListener("click", (event) => {
  // Do nothing if it's not a modal dialog
  if (!dialog.matches("dialog:modal")) return;

  // Close the <dialog> if clicked outside, i.e. on the <html> element
  if (event.target === dialog.ownerDocument.documentElement) {
    dialog.close();
  }
});

(Bonus: By avoiding ::backdrop, animation also becomes easier, since any animation technique used for the main <dialog> will automatically also cover the custom backdrop).

What do?

The <dialog> element’s ::backdrop is workable, with some elbow grease. I can live with it, and I’m being forgiving because <dialog> has been around for a while.

The popover attribute’s ::backdrop is unusable in its current state. I would create a custom backdrop, using something like an empty <div>.

Footnotes

  1. You might think that the presence of the dialog itself is a clue that the rest of the page is inert. However, this is not true for everyone. Users who browse the web at a high zoom level or using screen magnifiers may only be able to see a limited portion of the screen, which means the dialog could be off-screen when the user is looking at the content beneath the backdrop.

  2. User-agent styles declared with !important cannot be overridden by authors, even if the author styles are declared with !important.

  3. Another problem with blurring the backdrop like this is that it can obscure focus, violating WCAG SC 2.4.11. This is easily reproducible when you open the popover using your keyboard or when you Tab out of it.

  4. The lack of a backdropclick event seems to be an intentional choice. There’s some discussion around adding light dismiss functionality to <dialog>, avoiding the need for authors to listen for backdrop clicks in some cases. I’m wary of this promise. Even looking past the bad track record, I worry that the platform might decide that “light dismiss” includes interactions that don’t fit my personal definition of “light dismiss”, in which case I’m forced to implement my own custom backdrop.

  5. It might feel weird to listen for clicks on the <dialog>, but it makes sense when you think about it: ::backdrop is a pseudo-element, so of course the event listeners will go on the parent <dialog>. I created a CodePen some time ago if you’re interested in the full code.

  6. Side note: I don’t particularly like that I have to use matches to check if the dialog was opened using .showModal(). I also don’t like that there is no "open" event I can listen to, even though there is a "close" event.