The HTML <dialog> Element Explained

Modals are common on websites. We use them for confirmation messages, forms, settings, warnings, and many other small tasks.

They are also easy to build badly.

A custom modal can need a lot of code to handle focus, the Escape key, the page behind it, and the dark overlay. Modern HTML gives us a better starting point with the native <dialog> element.

It does not remove JavaScript completely, but it makes a good modal much easier to build.

A basic dialog

The HTML is simple:

<dialog>
  <h2>Hello</h2>
  <p>This is a dialog.</p>
</dialog>

A dialog is closed by default, so it will not be shown yet.

You can make it visible with the open attribute:

<dialog open>
  <h2>Hello</h2>
  <p>This dialog is open.</p>
</dialog>

This shows a non-modal dialog. It does not block the rest of the page.

For most normal modal windows, JavaScript’s showModal() method is the better choice.

Opening a modal

Start with:

<button id="open-dialog">
  Open dialog
</button>

<dialog id="example-dialog">
  <h2>Example dialog</h2>
  <p>This is a native HTML dialog.</p>
</dialog>

Then:

const dialog = document.querySelector("#example-dialog");
const openButton = document.querySelector("#open-dialog");

openButton.addEventListener("click", () => {
  dialog.showModal();
});

When showModal() is used, the dialog appears above the page as a modal.

Closing it

You can close it with JavaScript:

dialog.close();

But HTML gives us a nicer option for many cases.

Put a button inside a form with method="dialog":

<dialog id="example-dialog">
  <h2>Example dialog</h2>

  <p>This is a native HTML dialog.</p>

  <form method="dialog">
    <button>Close</button>
  </form>
</dialog>

Clicking the button closes the dialog automatically.

Styling the dialog

A dialog is just an HTML element, so normal CSS works:

dialog {
  width: min(32rem, calc(100% - 2rem));
  padding: 2rem;

  border: 0;
  border-radius: 0.75rem;
}

You can also style the page behind it with ::backdrop:

dialog::backdrop {
  background: rgb(0 0 0 / 0.5);
}

That replaces the extra overlay <div> that older modal examples often used.

A confirmation dialog

Here is a common example:

<button id="delete-button">
  Delete project
</button>

<dialog id="delete-dialog">
  <h2>Delete project?</h2>

  <p>
    This cannot be undone.
  </p>

  <form method="dialog">
    <button value="cancel">
      Cancel
    </button>

    <button value="delete">
      Delete
    </button>
  </form>
</dialog>

Open it:

const deleteButton = document.querySelector("#delete-button");
const deleteDialog = document.querySelector("#delete-dialog");

deleteButton.addEventListener("click", () => {
  deleteDialog.showModal();
});

The button value can be read through the dialog’s returnValue.

deleteDialog.addEventListener("close", () => {
  if (deleteDialog.returnValue === "delete") {
    console.log("Delete the project");
  }
});

This keeps the dialog logic very small.

show() and showModal()

There are two main ways to open a dialog.

dialog.show();

This opens a non-modal dialog. The rest of the page can still be used.

dialog.showModal();

This opens a modal dialog. The user is expected to deal with the dialog before going back to the page.

For most things people call a “modal”, showModal() is what you want.

The Escape key

One of the nice parts of native dialog is that the browser can handle normal dismiss behaviour, including the Escape key for modal dialogs.

With a fully custom modal, you would often need code like:

document.addEventListener("keydown", event => {
  if (event.key === "Escape") {
    closeModal();
  }
});

Using <dialog> means you do not have to rebuild all of that yourself.

Listening for cancel

If you need to know when the user tries to cancel the dialog, listen for the cancel event:

dialog.addEventListener("cancel", () => {
  console.log("Dialog was cancelled");
});

You can also stop the default closing behaviour:

dialog.addEventListener("cancel", event => {
  event.preventDefault();
});

Use that carefully. Users generally expect Escape to close a modal.

A contact form example

A dialog can contain a normal form:

<button id="contact-button">
  Contact me
</button>

<dialog id="contact-dialog">
  <h2>Contact me</h2>

  <form action="/contact" method="post">
    <label>
      Name
      <input type="text" name="name" required>
    </label>

    <label>
      Email
      <input type="email" name="email" required>
    </label>

    <label>
      Message
      <textarea name="message" required></textarea>
    </label>

    <button type="submit">
      Send
    </button>
  </form>

  <form method="dialog">
    <button>Close</button>
  </form>
</dialog>

The form itself works like any other form.

Do not put everything in a dialog

A dialog is useful when the content is temporary and focused.

It is not a replacement for a normal page.

If a form is large, has many steps, or contains important information that users may want to link to or come back to, a normal page is often better.

A good rule is simple: if the content feels like its own page, it probably should be one.

Dialog versus popover

The newer Popover API can look similar to a dialog, but the use is different.

Use a popover for lighter things such as:

Use a dialog for things such as:

A modal dialog asks for the user’s attention. A popover usually does not.

Browser support

The <dialog> element has been widely available across browsers since 2022.

That means it is no longer an experimental choice for most modern sites.

As always, check the browser versions your own users need, especially if you support old devices.

A complete example

<button id="newsletter-button">
  Join newsletter
</button>

<dialog id="newsletter-dialog">
  <h2>Join the newsletter</h2>

  <form action="/subscribe" method="post">
    <label for="newsletter-email">
      Email
    </label>

    <input
      id="newsletter-email"
      type="email"
      name="email"
      required
    >

    <button type="submit">
      Subscribe
    </button>
  </form>

  <form method="dialog">
    <button>Cancel</button>
  </form>
</dialog>
const button = document.querySelector("#newsletter-button");
const dialog = document.querySelector("#newsletter-dialog");

button.addEventListener("click", () => {
  dialog.showModal();
});
dialog {
  width: min(30rem, calc(100% - 2rem));
  padding: 2rem;
  border: 0;
  border-radius: 1rem;
}

dialog::backdrop {
  background: rgb(0 0 0 / 0.45);
}

That is a useful modal with very little custom code.

A better starting point

The main reason I like <dialog> is not that it removes every line of JavaScript. It does not.

The useful part is that it gives you a browser feature made for dialogs instead of asking you to recreate dialog behaviour with a pile of <div> elements.

For simple modals, that is a much better place to start.