Two people having a <dialog>

With Safari (15.4) being one of the last to implement the dialog element, a lot of browsers have great support for this element.. Goodbye to huge JavaScript libraries and welcome to the native HTML5 <dialog> element. This is beauty and simplicity on the web in its purest form. It’s accessible, customisable and most of all: easy to use.

Why we benefit from a dialog element in HTML5

Dialogs, modals, whatever you want to call them, they are everywhere. We use them for different reasons and they can become a hassle from time to time. When using third party libraries we turn to “css hacking” to get the look & feel we want. We load tons of JavaScript into our platforms for even the smallest of dialog use cases and last but not least, a lot of times, the dialogs we implement aren’t even accessible. Let’s get a bit more into detail.

The <dialog> element is native

Every major browser has support for this element. They work the same way and have more or less the same user agent styling. There are times when we need support for older browsers or previous releases of the modern ones. When that’s the case we can still rely on a polyfill to take care of this. By adding the “open” attribute to the modal, we can define if it’s visible or not.

The <dialog> is styleable

Rounded corners, sticky headers, you name it. We can style everything in a dialog element and from what I read on the w3c interactive elements spec: it supports flow content, this means that most elements that are used in the body of documents and applications can be used inside of it. This opens a lot of possibilities.

The <dialog> can have a ::backdrop

If… you open it with a trigger in JavaScript, you can get a pseudo element ::backdrop which you can style. The backdrop is by default not clickable to close the dialog, however there are some ways that we can do that. I’ll give an example further in the article.

The <dialog> is accessible

It automatically shifts your next tabfocus inside the dialog element itself and in Chrome it seems to have some sort of focus trapping, at least in my Codepen examples. VoiceOver also makes it clear when you enter a dialog element.

The <dialog> only needs a small amount of JavaScript

Just some easy triggers that you need to call and you’re ready to go. Of Course you can expand on that by adding new click events on the backdrop and things like that. It’s a real front-end performance boost compared to the huge libraries we used to implement.

Playing around with the dialog element.

I created the following codepen if you want to play around with it: Dialog with sticky header in HTML
There are many ways to trigger a dialog but in its purest form, I created the following:

<button class="btn btn-primary" data-target="my-dialog" data-modeltrigger>
  Click me
</button>
<dialog data-name="my-dialog">
  <header>
    <h2>Well hello there!</h2>
    <button class="btn btn-icon" data-target="my-dialog" data-modeltrigger>
      <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
        <path stroke-linecap="round" stroke-linejoin="round" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
      </svg>
    </button>
  </header>
  <main>
    <p>I am a lovely dialog with HTML. This is a really awesome HTML feature don't you think? Only requires a bit of javascript and so versatile.</p>
    <p>I even have a great scroll behaviour right outside the box :) check it out by editing this text</p>
  </main>
  <footer>
    <button class="btn btn-primary-outline" data-target="my-dialog" data-modeltrigger>
      close
    </button>
  </footer>
</dialog>

By adding some data attributes I created the following triggers in my Vanilla JS::

  • data-modeltrigger specifies that this element (button) triggers a modal
  • data-target gives a reference to the name of the dialog i want to trigger
  • data-name is added on the dialog which is referred to by the data target. (I chose not the use id’s)
const triggers = document.querySelectorAll("[data-modeltrigger]");

triggers.forEach(function (el) {
  el.addEventListener("click", function () {
    const getTarget = el.getAttribute("data-target");
    const target = document.querySelector(`[data-name="${getTarget}"]`);
    if (target.hasAttribute("open")) {
      target.close();
    } else {
      target.showModal();
    }
  });
});

Inside the javascript I added a little check to see if the modal is already open or not, this way, creating buttons that open or close the modal are made easy.

To wrap it up, I added a bit of custom styling to the modal as well, here is the main part of it:

/* Styling the modal and backdrop */

dialog {
  width: 100%;
  max-width: 600px;
  padding: 0;
  background: #fff;
  border-radius: 8px;
  border: 1px solid #dedede;
  box-shadow: rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;
  animation: scale-in 0.4s ease-out;
}

dialog::backdrop {
  background: radial-gradient(
    circle,
    rgba(216, 209, 116, 0.5) 0%,
    rgba(182, 196, 84, 0.7) 100%
  );
}

/* bouncy animation because i can */
@keyframes scale-in {
  0% {
    opacity: 0;
  }
  50% {
    opacity: 1;
    transform: scale(0.9);
  }
  100% {
    transform: scale(1);
  }
}

/* Styling inside of the modal in the demo */
A simple dialog element
And so, a first <dialog> demo has been made.

Clicking the ::backdrop to close the modal

I haven’t really found a beautiful way to handle this. It seems that at the moment the easiest way to do this is to use the getBoundingClientRect() to check if a person is clicking outside of the modal and when doing so, trigger a close() on the dialog element.

Update: On LinkedIn, someone showed me a more elegant solution for closing the dialog based on the following tweet by Adam Argyle. This is much cleaner than calculating the mouse position.

This is the JS I added (updated):

const dialogs = document.querySelectorAll("dialog");

/* Check for click in backdrop */
dialogs.forEach(function (el) {
  el.addEventListener("click", ({target:dialog}) => {
    if (dialog.nodeName === 'DIALOG') {
      dialog.close('dismiss')
    }
  });
});

See the codepen: Dialog with backdrop click close

Taking things overboard: A dialog in a dialog in a dialog…

A dialog inside a dialog

If you read a few of my “trying out” posts before, you know that I like to play around with things, maybe break them, or just create something crazy. This is just me doing that. I created a little demo which nested the dialog elements inside of each other, this should be valid, remember: “flow content” is possible in dialog elements.

In the end it’s just a bunch of dialogs triggering each other and a newly added data-parenttrigger attribute which force closes all the other dialogs on the page. By adding this little bit of JavaScript on it:

/* 
...
inside the trigger click event..
*/

if (el.getAttribute("data-parenttrigger") !== null) {
  dialogs.forEach(function (el) {
    el.close();
  });
}

See the demo “A dialog inside a dialog inside a dialog” if you want a little laugh.

One important thing I noticed here: The backdrops of these dialogs seem to stack upon each other because they're not pseudo elements to the dialog and not dependant on the page, so if you feel inspired by that, you might create something “visually interesting”.

Final <monologue> on the <dialog>

Being a developer for quite some time, I had my fair share of dialogs. Hacking in JS and CSS to do something special with them that wasn’t supported by the library which provided them. Accessibility issues that just didn’t seem to find a right answer. I’m really happy with this addition and I hope it gets implemented by web developers around the world in the future. I’ve also been playing a lot of Elden Ring in my spare time, so “praise the message” (Elden Ring players will get this ;) )

 in  CSS , HTML , Javascript