Ronalds Vilciņš

Building a modal with HTML dialog

A modal is easy to draw and surprisingly hard to finish. Focus, the Escape key, the page behind it, and the backdrop all need behaviour, not just CSS.

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.

Open a native modal and return focus
View code
HTML
<button id="open" type="button">Open dialog</button><button type="button">Another page control</button>
<p>While the modal is open, try Tab and Escape. Closing it returns focus to the opener.</p>
<dialog aria-labelledby="dialog-title"><h2 id="dialog-title">A native modal</h2><p>The background is inactive within this demo frame.</p><label for="note">A note</label><input id="note" type="text"><p><button id="close" type="button">Close dialog</button></p></dialog>
<p class="note">The modal and its backdrop stay inside the iframe.</p>
CSS
button, select { padding: .35rem .6rem; border: 1px solid var(--clg); background: var(--cw); color: var(--cb); }
input[type="range"] { width: min(100%, 18rem); vertical-align: middle; }
h2, h3 { font-size: 1rem; }
.note { color: var(--clg); }

dialog { width: min(26rem, calc(100% - 2rem)); padding: 1rem; border: 1px solid var(--clg); background: var(--cw); color: var(--cb); }
dialog::backdrop { background: rgb(0 0 0 / .4); }
dialog h2 { margin-top: 0; }
input { max-width: 100%; }
label { display: block; margin-bottom: .25rem; }
JavaScript
const dialog = document.querySelector('dialog');
document.querySelector('#open').addEventListener('click', () => dialog.showModal());
document.querySelector('#close').addEventListener('click', () => dialog.close());

Begin with an actual dialog

The HTML is simple:

HTML
<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:

HTML
<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:

HTML
<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:

JavaScript
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:

JavaScript
dialog.close();

But HTML gives us a nicer option for many cases.

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

HTML
<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:

CSS
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:

CSS
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:

HTML
<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:

JavaScript
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.

JavaScript
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.

JavaScript
dialog.show();

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

JavaScript
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:

JavaScript
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:

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

You can also stop the default closing behaviour:

JavaScript
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:

HTML
<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

HTML
<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>
JavaScript
const button = document.querySelector("#newsletter-button");
const dialog = document.querySelector("#newsletter-dialog");

button.addEventListener("click", () => {
  dialog.showModal();
});
CSS
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 a simple modal, I would begin with <dialog> and add only the behaviour the design still needs. A pile of <div> elements should not be the default starting point for a control the browser already understands.

Filed under , and on

Last updated on

Enjoyed this article?

If you found it useful, share it with a friend or someone who might find it helpful too. Thanks for reading!

← Back to all articles