Safari has finally adopted the <dialog> HTML element, and now it is supported by all browsers!

HTML <dialog> is basically a “modal”:

  • centered on page by default
  • disables background clicks when open
  • can be closed with native HTML (without extra JS)
  • includes CSS to blur/dim background by default
  • can be closed with Escape key

Example:

open-close-html-dialog-modal

Display basic dialog:

<dialog open>
  <span>You can see me</span>
</dialog>

Dialog with “Close” button (without submitting form):

method="dialog" on form

<dialog open>
  <span>You can see me</span>
  <form method="dialog">
    <button type="submit" autofocus>Cancel</button>
  </form>
</dialog>

Dialog with both “Close” button and regular “Submit” button on form:

formmethod="dialog" on button

<dialog open>
  <span>You can see me</span>
  <form>
    abc
    <button formmethod="dialog" type="submit">Cancel</button>
    <button>Submit</button>
  </form>
</dialog>

With button to open modal:

<div data-controller="dialog">
  <button data-action="dialog#open">
    Open modal
  </button>

  <dialog data-dialog-target="modal">
    <span>You can see me</span>
    <form method="dialog">
      <button type="submit" autofocus>Cancel</button>
    </form>
  </dialog>
</div>
// app/javascript/controllers/dialog_controller.js
static targets = ["modal"]

open() {
  this.modalTarget.showModal()
  // this.modalTarget.show()
}
  • .show() - background is clickable, can be used like a regular dropdown
  • .showModal() - background is not clickable, you can apply css styles like blur. You can use “Esc” key close it!

Most likely you want to use exclusively .showModal().

Background blur, color, opacity

dialog::backdrop {
  backdrop-filter: blur(8px);
  background-color: hsl(250, 100%, 50%, 0.25);
}

Close on click outside

  clickOutside(event) {
    if (event.target === this.dialogTarget) {
      this.close()
    }
  }
-<div data-controller="dialog">
+<div data-controller="dialog" data-action="click->dialog#clickOutside">

Disable background scrolling when dialog is open

  open() {
    this.dialogTarget.showModal()
    document.body.classList.add("overflow-hidden");
  }

  close() {
    this.dialogTarget.close()
    document.body.classList.remove("overflow-hidden");
  }

Important: It will not work with the default behaviour of closing by clicking Escape or by method="dialog".

To make it actually work you need to listen to the close event on <dialog>:

  connect() {
    this.modalTarget.addEventListener("close", this.enableBodyScroll.bind(this))
  }

  enableBodyScroll() {
    document.body.classList.remove('overflow-hidden')
  }

Blur background

/* app/assets/stylesheets/application.css */
dialog::backdrop {
  backdrop-filter: blur(8px);
  /* background-color: hsl(250, 100%, 50%, 0.25); */
}

Final result

  • ✅ Styled modal
  • ✅ Blur background
  • ✅ Close on Escape
  • ✅ Close on click outside
  • ✅ Close on clicking button

open-close-html-dialog-modal

<div data-controller="dialog" data-action="click->dialog#clickOutside">
  <button data-action="click->dialog#open">Open dialog</button>
  <dialog data-dialog-target="modal"
          class="backdrop:bg-gray-400 backdrop:bg-opacity-90 z-10 rounded-md border-4 bg-sky-900 w-full md:w-2/3 mt-24">
    <div class="p-8">
      <button class="bg-slate-400" data-action="dialog#close">Cancel</button>
      <p>Greetings, one and all!</p>
      <form>
        <button formmethod="dialog">Cancel</button>
        <button>OK</button>
      </form>
    </div>
  </dialog>
</div>
// app/javascript/controllers/dialog_controller.js
import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="dialog"
export default class extends Controller {
  static targets = ["modal"]

  connect() {
    this.modalTarget.addEventListener("close", this.enableBodyScroll.bind(this))
  }

  disconnect() {
    this.modalTarget.removeEventListener("close", this.enableBodyScroll.bind(this))
  }

  open() {
    // this.modalTarget.show()
    this.modalTarget.showModal()
    document.body.classList.add('overflow-hidden')
  }

  close() {
    this.modalTarget.close()
    // document.body.classList.remove('overflow-hidden')
  }

  enableBodyScroll() {
    document.body.classList.remove('overflow-hidden')
  }

  clickOutside(event) {
    if (event.target === this.modalTarget) {
      this.close()
    }
  }
}

Inspired by:

  • https://blog.webdevsimplified.com/2023-04/html-dialog/
  • https://dev.to/thomasvanholder/create-a-modal-with-the-html-dialog-element-tailwind-and-stimulus-573b

To explore in the future:

  • submitting a form with errors
  • submitting a form with format.html, format.turbo_stream

That’s it for now! 🤠