How to Implement and Style the Dialog Element

Go in-depth on the native dialog HTML element with this tutorial and learn how to implement a user-friendly, accessible dialog on your website.

A dialog is a component of web interfaces providing important information and requiring user interaction in response. The user’s attention should be focused exclusively on the active dialog element, both visually and interactively. Information and user actions presented by the dialog should be phrased simply and unambiguously. So, a dialog is interruptive by nature and should be used sparingly.

Usage

Imagine a user browsing a web application from a mobile phone. The application needs the user’s decision about its settings immediately in order to keep functioning properly – like enabling location services in order to give directions on a map. This could be a use case for a dialog:

  1. The dialog pops up. Only the dialog is interactive, lying over the rest of the content.
  2. The dialog’s header explains the required actions in short. The dialog’s body may contain more detailed information.
  3. One or more interactive elements provide possible user actions in order to find a solution.

Structure

A modal dialog consists of a container, title, description, buttons and a backdrop. If the user’s flow browsing the application must be interrupted anyway, the least we can do is to present the user a concise and well structured, clear dialog to attract their focus and quickly make an action in order to continue browsing.

The basic structure of a modal dialog element.
Basic structure of a modal dialog

It’s essential to phrase a clear and unambiguous message in the title, so the reader can understand it at a glance. Here is one example:

  • Not such a good title: “Do you want to proceed?”
  • Better, because direct and clear: “Allow access to the file system?”

Of course, further information can be put in the body of the dialog itself, but the gist should be comprehensible by reading the title and button texts only.

Behavior

A dialog always needs to suit a notable purpose: Getting the user to make a choice in order to finish a task or to keep the application functioning properly (like enabling location services for navigation).

Should the dialog close by clicking the backdrop?

Well, I only asked myself this question after trying to implement that behavior with the native dialog element. As it turns out, it’s far easier with ordinary divs to achieve.

Without the native dialog element, your markup would look something like this:

<div id="dialog" role="dialog" aria-modal="true">
  <!-- Your dialog content -->
</div>
<div class="backdrop"></div>

And the corresponding CSS

.backdrop {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
}

Here we have an ordinary div stretching out over the full viewport. We can easily grab that div.backdrop with JavaScript and implement our “close-modal-on-click-behavior”.

const backdrop = document.querySelector(".backdrop");
const dialog = document.getElementById("dialog");

backdrop.addEventListener("click", function() { dialog.style.display = none; });

So, why can’t we do exactly this with the native dialog element?

The native dialog element comes with a pseudo-element called ::backdrop when invoked with dialog.showModal(). As the name suggests, it is not part of the DOM, and so we cannot access it using JavaScript…

How can we add an event listener on an element, which is essentially not part of the DOM? Well, there are workarounds, like detecting a click outside of the active dialog, but that’s a completely different story.

And once I’ve come to understand, that it is not that easy, I’ve revisitied the initially posed question: Is it worthwhile to close the dialog on click outside?

No, it is not. Keep in mind, that we wanted the user to make a decision. We interrupted the user’s flow of browsing the application. We phrased the message clearly and directly, so that it’ll be comprehensible at a glance. And then we allow the user to dismiss everything we have carefully put together with a single click?! I don’t think so.

Implementation

When we implement a dialog, the following requirements must be observed carefully:

  • Focus the first interactive element inside the modal once the dialog opens (*)
  • Trap focus inside the modal dialog
  • Provide at least one button that closes the dialog
  • Prevent interaction with the rest of the page (*)
  • Close the dialog when the user presses ESC (*)
  • Return focus to the element, that opened the dialog in the first place

Requirements marked with (*) are handled by the native dialog element out of the box, when opened as a modal.

So in order to get all the benefits listed above, we’re going to invoke the dialog using the method showModal provided to us by the native dialog JavaScript API.

// Open dialog as a modal
const dialog = querySelector("dialog");
dialog.showModal(); 

Example HTML structure

<button id="open_dialog">Open Dialog</button>

<dialog
  aria-labelledby="dialog_title"
  aria-describedby="dialog_description"
>
  <img
    src="./location-service.svg"
    alt="Illustration of Location Services"
  />
  <h2 id="dialog_title" class="h2">Use location services?</h2>
  <p id="dialog_description">
    In order to give directional instructions, we kindly ask you to turn
    on the location services.
  </p>
  <div class="flex flex-space-between">
    <button id="close_dialog">Close</button>
    <button id="confirm_dialog" class="cta">Confirm</button>
  </div>
</dialog>

Because we’re using the native dialog element here, we do not need to use role="dialog", modal="true" or similar for an accessible implementation.

Based on this simple HTML structure, which is taken from the example CodePen shown at the end of this article, we can now go ahead and implement the requirements listed above. Once the reader clicks the “Open Dialog” button, the first interactive element inside the dialog will receive focus by default.

Return focus to last active element after closing the dialog

The HTML of a modal dialog can be placed nearly anywhere in the page’s markup. So, when the reader opens the modal, the user agent jumps to the dialog’s markup, like using a portal. Once the reader closes the dialog again, the focus needs to be returned back to the element that the reader was interacting with before opening the dialog. The portal to and from the dialog should go two-way, otherwise the reader will get lost.

const dialog = document.querySelector("dialog");
const openDialogBtn = document.getElementById("open_dialog");
const closeDialogBtn = document.getElementById("close_dialog");

const openDialog = () => {
  dialog.showModal();
};

const closeDialog = () => {
  dialog.close();

  // Returns focus back to the button
  // that opened the dialog
  openDialogBtn.focus();
};

openDialogBtn.addEventListener("click", openDialog);
closeDialogBtn.addEventListener("click", closeDialog);

// If the buttons of the dialog are contained inside a <form>
// Use event.preventDefault()
const closeDialog = (event) => {
  event.preventDefault();
  dialog.close();
  openDialogBtn.focus();
};

Trap focus inside the dialog while open

A focus trap is often a horror regarding UX – in case of a modal dialog it serves an essential purpose: Keeping the reader’s focus on the dialog, helping to prevent interaction with the background.

Based on the same markup and existing JS above, we can add the focus trap to our script.

const trapFocus = (e) => {
  if (e.key === "Tab") {
    const tabForwards = !e.shiftKey && document.activeElement === lastElement;
    const tabBackwards = e.shiftKey && document.activeElement === firstElement;

    if (tabForwards) {
      // only TAB is pressed, not SHIFT simultaneously
      // Prevent default behavior of keydown on TAB (i.e. focus next element)
      e.preventDefault();
      firstElement.focus();
    } else if (tabBackwards) {
      // TAB and SHIFT are pressed simultaneously
      e.preventDefault();
      lastElement.focus();
    }
  }
};

// Attach trapFocus function to dialog on keydown
// Updated openDialog
const openDialog = () => {
  dialog.showModal();
  dialog.addEventListener("keydown", trapFocus);
};

// Remove trapFocus once dialog closes
// Updated closeDialog
const closeDialog = () => {
  dialog.removeEventListener("keydown", trapFocus);
  dialog.close();
  openDialogBtn.focus();
};

Disable closing the dialog on ESC

Just in case you want to disable the built-in functionality of closing the dialog once the user has pressed the ESC key, you can listen for the keydown event when the dialog opens and prevent its default behavior. Please remember to remove the event listener after the modal has closed.

Here is the code to make it happen:

// Inside the function that calls dialog.showModal()
const dialog = document.querySelector("dialog");

const openDialog = () => {
  // ...
  dialog.addEventListener("keydown", (e) => {
    if (e.key === "Escape") {
      e.preventDefault();
    }
  });
};

Styles for the dialog element

The user agents provide some default styles for the dialog element. To override these and apply our own styles, we can use this tiny CSS reset.

dialog {
  padding: 0;
  border: none !important;
  /* !important used here to override polyfill CSS, if loaded */
}

Admittedly, there are more default user agent styles, which center the dialog inside the viewport and prevent overflowing content. We’ll leave these default styles untouched, because in our case they are desirable.

CSS ::backdrop pseudo-element

Perhaps the coolest thing about the native dialog element is, that it gives us a nice ::backdrop pseudo-element right out of the box. The serves several purposes for us:

  • Overlay to prevent interaction with the background
  • Easily style the surroundings of the dialog while open

Accessibility aspects of a dialog element

To ensure accessibility of your modal dialog you’ve already got a great deal covered by simply using the native HTML dialog element as a modal – i.e. invoked with dialog.showModal(). Thus, the first interactive element will receive focus, once the dialog opens. Additionally, interaction with other content on the page will be blocked while the dialog is active. Plus, the dialog closes with a keystroke on ESC. Everything coming “for free” along with the native dialog element.

In contrast to using a generic div as a wrapper instead of the semantically correct dialog element, you do not have to put role="dialog" accompanied by aria-modal="true.

Apart from these benefits the native dialog element has to offer, we need to make sure the following aspects are implemented:

  • Put a label on the dialog element – e.g. <dialog aria-label="Use location services?"> or use aria-labelledby if you want to reference the ID of another element inside the dialog’s body, which presents the title anyway
  • If the dialog message requires additional information, which may already be visible in the dialog’s body, you can optionally reference this text with aria-describedby or phrase a description just for screen readers inside an aria-description
  • Return focus to the element, which opened the dialog in the first place, if the dialog has been triggered by a click interaction. This is to ensure that the user can continue browsing the site or application from the same point of regard where they left off before the dialog popped up.

Polyfill for the native dialog element

Sadly, the native HTML dialog element still lacks browser support here and there. As of this writing, Chrome, Edge and Opera support it, Firefox hides support behind a flag. No support from Safari and IE. The support coverage is around 75% globally. Reference browser support

On the bright side, the dialog element is easily polyfilled with this dialog polyfill from GoogleChrome.

In order to load the polyfill only on those browsers not supporting the dialog element, we check if dialog.showModal is not a function.

const dialog = document.querySelector("dialog");

if (typeof dialog.showModal !== "function") {
  // Load polyfill script
  const polyfill = document.createElement("script");
  polyfill.type = "text/javascript";
  polyfill.src = "dist/dialog-polyfill.js"; // example path
  document.body.append(polyfill);

  // Register polyfill on dialog element once the script has loaded
  polyfill.onload = () => {
    dialogPolyfill.registerDialog(dialog);
  };

  // Load polyfill CSS styles
  const polyfillStyles = document.createElement("link");

  polyfillStyles.rel = "stylesheet";
  polyfillStyles.href = "dialog-polyfill.css";
  document.head.append(polyfillStyles);
}

Example of a styling a native dialog element

Here is a CodePen showing off an accessible, polyfilled modal dialog. It implements the requirements listed above regarding accessibility, managing focus and polyfill on-demand. The style is based on Giovanni Piemontese’s Auto Layout Dialogs – Figma UI Kit.

See the Pen Accessible Material Dialog by Christian Kozalla (@ckozalla) on CodePen.

Apart from CodePen, you can view the source code of the example here on GitHub. A live example of that native dialog is hosted here.

Wrapping up

In this tutorial discussed the structure and purpose of dialogs regarding user-experience, especially for modal dialogs. We’ve compiled a list of requirements for creating user-friendly dialogs. Naturally, we’ve gone in-depth on the native dialog HTML element and the benefits we gain from using it. We’ve extended its functionality by building a focus trap and managing focus around the life-cycle of the native dialog altogether.

We’ve seen how to implement an accessible modal dialog based on the requirements we set before. Our implementation will be polyfilled only when necessary.

Finally, I’ve noticed during my research about the native dialog element, that its reputation in the community has changed alot over the years. It may have been welcomed with an open mind, but today’s opinions are predominantly criticizing the dialog element while simultaneously advising to rather rely on libraries.

Nevertheless, I’m sure the native dialog element has proven to be a suitable basis for implementing modal dialogs in this tutorial. I definitely had some fun!

Thanks for reading, I hope you enjoyed it!

Related

Another tutorial that might be interesting to you is this one by Osvaldas Valutis, where you’ll learn how to style and customize the upload button (file inputs).

Christian Kozalla

Web developer with a passion for full-stack JavaScript and a weakness for licorice and tender guitar music

Stay in the loop: Get your dose of frontend twice a week

👾 Hey! Looking for the latest in frontend? Twice a week, we'll deliver the freshest frontend news, website inspo, cool code demos, videos and UI animations right to your inbox.

Zero fluff, all quality, to make your Mondays and Thursdays more creative!

Feedback 4

Comments are closed.
  1. For your implementation notes, I recommend against a blanket assertion that the first focusable element should get focus in all cases. Consider the Terms of Service example Scott O’Hara uses:
    https://scottaohara.github.io/testing/html-dialog/tos-dialog.html

    In fact, with the exception of Chrome (which continues to drag its feet after 3 years), browser makers are on board with defaulting to placing focus on the dialog itself after listening to UX and accessibility experts:
    https://github.com/whatwg/html/pull/4184

    If rolling your own dialog, preventing interaction with the underlying page could be achieved with the `inert` polyfill (which may be less overhead and risk of screen reader bypasses than your focus trap script offers):
    https://github.com/GoogleChrome/inert-polyfill

    As for the native dialog polyfill, it does not work:
    https://www.scottohara.me/blog/2019/03/05/open-dialog.html

    Finally, to prevent a WCAG SC 1.4.10 Reflow, I suggest adding `flex-wrap: wrap;` to your `.flex` class on line 121. This will prevent the buttons from overlapping.
    https://www.w3.org/TR/WCAG21/#reflow

    • Hey Adrian,

      thanks for your kind suggestions!

      1. Of course, a blanket assertion like this is not always suitable for every use-case – just like for Scott’s ToS Example. Laying focus on the first interactive element is the default behavior of the dialog element. But that’s not always useful, like you pointed out.

      It is possible to override the default behavior by programmatically focusing elements or using tabindex.
      As stated here in the notes: https://www.w3.org/TR/wai-aria-practices-1.1/#dialog_modal

      “If content is large enough that focusing the first interactive element could cause the beginning of content to scroll out of view, it is advisable to add tabindex=’-1′ to a static element at the top of the dialog, such as the dialog title or first paragraph, and initially focus that element.”

      2. Placing focus on the dialog itself by default seems like a great solution

      3. Great! This, indeed, seems to be a replacement for a focus trap.

      4. Regarding WCAG 1.4.10 Reflow – Thanks for the tip!

  2. The question about whether to implement (or allow or support) “close on click outside” behavior is not an all-or-nothing choice. Not all dialogs require the user to make a choice that affects the remainder of the process flow; some dialogs simply display (a perhaps complex set of) information, typically at the user’s request. These sorts of dialogs are not “short-circuited” when closed by a click outside of the dialog, since they aren’t really “interrupting” a process flow and do not require a “response” from the user.

    • Hey John,

      You’re right. It depends on the use-case, like you thoroughly explained.
      In the article I merely considered dialog modals used in an application, but even there situations like you describe can occur.