@mdo

Fun with the dialog element

There’s a new element in town and it’s the <dialog>. Support for it just landed in Safari with their newest update (v15.4) across all Apple’s devices. With that update, the <dialog> element is now supported in every evergreen browser. It’s a great step forward for frameworks and libraries looking to make better use of native behaviors, but it’s also not without its limits.

Default styles

Chrome, Safari, and Firefox all render dialogs similarly—black border, black text, white background. They’re all positioned absolutely without a predefined z-index. There’s also no default ::backdrop styling. Check out the CodePen demo.

Dialog element in Chrome

Dialog element in Safari

Dialog element in Firefox

Only minor differences in the border-width it seems.

When there’s too much content, all dialogs will have an inner scroll. This makes adding a modal header a little annoying as you’ll have to ether add position: sticky, or create some custom components within to handle the scroll.

Functionality

By default, there’s no HTML magic you can use to toggle <dialog> elements—you have to write some JavaScript to open and close them programmatically. However, you can force a <dialog> open with the open attribute, which you can see in action in my previously linked CodePen.

Here’s what else you can do with a <dialog>:

  • Open regular, absolutely positioned dialogs with the show() method if you don’t want a modal or backdrop
  • Open modal dialogs with the showModal() method to open fixed position dialogs that block other page interactions
  • Close dialogs with the close() method
  • Use Esc to close the dialog without any additional code
  • Style and animate the ::backdrop pseudo-element
  • Add a specific autofocus attribute inside the <dialog> to automatically focus on it (otherwise, the first focusable element will be focused)

Unfortunately, because the modal <dialog> backdrop is a pseudo-element, you cannot trigger JavaScript events off it. This makes it impossible to click the backdrop to dismiss the dialog. There’s likely a solution here with non-modal <dialog>s, but I haven’t implemented a smooth solution to this yet.

Regarding dialogs autofocus-ing on the first focusable element, I suggest including a dismiss button near the top. When dialogs are too long and they scroll, it will automatically jump to that focused element. That means if you only have a dismiss button or single action at the bottom of the dialog, your visitors will miss the top portion.

Suggested styling

Dialogs leave a lot to be desired with that default styling, but they are just as style-able as any other element. I put together a CodePen demo that shows some suggested styles and implements easy toggling and closing via JavaScript.

See the Pen Using the <dialog> element by Mark Otto (@emdeoh) on CodePen.

Most dialogs include an inner header element with title and dismiss button, so I’ve implemented the same into my CodePen demo. I’ve also the position: sticky style I mentioned earlier to the dialog header so that when the dialog has an inner scroll, the dialog title and dismiss button are always visible. The dismiss button here also traps that default autofocus. Lastly, the backdrop is styled with a translucent background-color and a quick little fade-in animation.

Lastly, I made it all dark mode friendly with the help of some CSS variables and the prefers-color-scheme media query.

Show and hide

On the JS side, showing and hiding the dialog is easy enough. In my demo, this is accomplished with data attributes:

  • data-toggle is added to any triggering element and it’s value is the specified target <dialog>
  • data-close is a boolean attribute that finds the nearest <dialog> to hide

Here’s the complete JS in action.

let toggler = document.querySelectorAll('[data-toggle]')
let closers = document.querySelectorAll('[data-close]')

if (toggler) {
  toggler.forEach(function (element) {
    let target = element.getAttribute("data-toggle")
    let targets = document.querySelectorAll(target)

    element.addEventListener("click", (event) => {
      targets.forEach(function (e) {
        e.showModal()
      })
    })
  })

  closers.forEach(function (element) {
    element.addEventListener("click", (event) => {
      let dialog = element.closest('dialog')
      dialog.close()
    })
  })
}

You can see it all in the demo.

Details element

Before the <dialog> support for Safari dropped this week, I was playing with the <details> element to build dropdowns and modals. It was definitely a fun and worthwhile experiment, and there’s a lot more I want to explore here around keyboard navigation and better modal styling/positioning. For now though, it’s a good comparison to the new <dialog> element and how you can create custom HTML elements.

See the Pen Modals, dropdowns, and more by Mark Otto (@emdeoh) on CodePen.

Check it out on CodePen.

Resources