Dialog Components: Go Native HTML or Roll Your Own?

As the author of a library called AgnosticUI, I’m always on the lookout for new components. And recently, I decided to dig in and start work on a new dialog (aka modal) component. That’s something many devs like to have in their toolset and my goal was to make the best one possible, with an extra special focus on making it inclusive and accessible.

My first thought was that I would avoid any dependencies and bite the bullet to build my own dialog component. As you may know, there’s a new <dialog> element making the rounds and I figured using it as a starting point would be the right thing, especially in the inclusiveness and accessibilities departments.

But, after doing some research, I instead elected to leverage a11y-dialog by Kitty Giraudel. I even wrote adapters so it integrates smoothly with Vue 3, Svelte, and Angular. Kitty has long offered a React adapter as well.

Why did I go that route? Let me take you through my thought process.

First question: Should I even use the native <dialog> element?

The native <dialog> element is being actively improved and will likely be the way forward. But, it still has some issues at the moment that Kitty pointed out quite well:

  1. Clicking the backdrop overlay does not close the dialog by default
  2. The alertdialog ARIA role used for alerts simply does not work with the native <dialog> element. We’re supposed to use that role when a dialog requires a user’s response and shouldn’t be closed by clicking the backdrop, or by pressing ESC.
  3. The <dialog> element comes with a ::backdrop pseudo-element but it is only available when a dialog is programmatically opened with dialog.showModal().

And as Kitty also points out, there are general issues with the element’s default styles, like the fact they are left to the browser and will require JavaScript. So, it’s sort of not 100% HTML anyway.

Here’s a pen demonstrating these points:

Now, some of these issues may not affect you or whatever project you’re working on specifically, and you may even be able to work around things. If you still would like to utilize the native dialog you should see Adam Argyle’s wonderful post on building a dialog component with native dialog.

OK, let’s discuss what actually are the requirements for an accessible dialog component…

What I’m looking for

I know there are lots of ideas about what a dialog component should or should not do. But as far as what I was personally going after for AgnosticUI hinged on what I believe make for an accessible dialog experience:

  1. The dialog should close when clicking outside the dialog (on the backdrop) or when pressing the ESC key.
  2. It should trap focus to prevent tabbing out of the component with a keyboard.
  3. It should allow forwarding tabbing with TAB and backward tabbing with SHIFT+TAB.
  4. It should return focus back to the previously focused element when closed.
  5. It should correctly apply aria-* attributes and toggles.
  6. It should provide Portals (only if we’re using it within a JavaScript framework).
  7. It should support the alertdialog ARIA role for alert situations.
  8. It should prevent the underlying body from scrolling, if needed.
  9. It would be great if our implementation could avoid the common pitfalls that come with the native <dialog> element.
  10. It would ideally provide a way to apply custom styling while also taking the prefers-reduced-motion user preference query as a further accessibility measure.

I’m not the only one with a wish list. You might want to see Scott O’Hara’s article on the topic as well as Kitty’s full write-up on creating an accessible dialog from scratch for more in-depth coverage.

It should be clear right about now why I nixed the native <dialog> element from my component library. I believe in the work going into it, of course, but my current needs simply outweigh the costs of it. That’s why I went with Kitty’s a11y-dialog as my starting point.

Auditing <dialog> accessibility

Before trusting any particular dialog implementation, it’s worth making sure it fits the bill as far as your requirements go. With my requirements so heavily leaning on accessibility, that meant auditing a11y-dialog.

Accessibility audits are a profession of their own. And even if it’s not my everyday primary focus, I know there are some things that are worth doing, like:

This is quite a lot of work, as you might imagine (or know from experience). It’s tempting to take a path of less resistance and try automating things but, in a study conducted by Deque Systems, automated tooling can only catch about 57% of accessibility issues. There’s no substitute for good ol’ fashioned hard work.

The auditing environment

The dialog component can be tested in lots of places, including Storybook, CodePen, CodeSandbox, or whatever. For this particular test, though, I prefer instead to make a skeleton page and test locally. This way I’m preventing myself from having to validate the validators, so to speak. Having to use, say, a Storybook-specific add-on for a11y verification is fine if you’re already using Storybook on your own components, but it adds another layer of complexity when testing the accessibility of an external component.

A skeleton page can verify the dialog with manual checks, existing a11y tooling, and screen readers. If you’re following along, you’ll want to run this page via a local server. There are many ways to do that; one is to use a tool called serve, and npm even provides a nice one-liner npx serve <DIRECTORY> command to fire things up.

Let’s do an example audit together!

I’m obviously bullish on a11y-dialog here, so let’s put it to the test and verify it using some of the the recommended approaches we’ve covered.

Again, all I’m doing here is starting with an HTML. You can use the same one I am (complete with styles and scripts baked right in).

View full code
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>A11y Dialog Test</title>
    <style>
      .dialog-container {
        display: flex;
        position: fixed;
        top: 0;
        left: 0;
        bottom: 0;
        right: 0;
        z-index: 2;
      }
      
      .dialog-container[aria-hidden='true'] {
        display: none;
      }
      
      .dialog-overlay {
        position: fixed;
        top: 0;
        left: 0;
        bottom: 0;
        right: 0;
        background-color: rgb(43 46 56 / 0.9);
        animation: fade-in 200ms both;
      }
      
      .dialog-content {
        background-color: rgb(255, 255, 255);
        margin: auto;
        z-index: 2;
        position: relative;
        animation: fade-in 400ms 200ms both, slide-up 400ms 200ms both;
        padding: 1em;
        max-width: 90%;
        width: 600px;
        border-radius: 2px;
      }
      
      @media screen and (min-width: 700px) {
        .dialog-content {
          padding: 2em;
        }
      }
      
      @keyframes fade-in {
        from {
          opacity: 0;
        }
      }
      
      @keyframes slide-up {
        from {
          transform: translateY(10%);
        }
      }

      /* Note, for brevity we haven't implemented prefers-reduced-motion */
      
      .dialog h1 {
        margin: 0;
        font-size: 1.25em;
      }
      
      .dialog-close {
        position: absolute;
        top: 0.5em;
        right: 0.5em;
        border: 0;
        padding: 0;
        background-color: transparent;
        font-weight: bold;
        font-size: 1.25em;
        width: 1.2em;
        height: 1.2em;
        text-align: center;
        cursor: pointer;
        transition: 0.15s;
      }
      
      @media screen and (min-width: 700px) {
        .dialog-close {
          top: 1em;
          right: 1em;
        }
      }
      
      * {
        box-sizing: border-box;
      }
      
      body {
        font: 125% / 1.5 -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif;
        padding: 2em 0;
      }
      
      h1 {
        font-size: 1.6em;
        line-height: 1.1;
        font-family: 'ESPI Slab', sans-serif;
        margin-bottom: 0;
      }
      
      main {
        max-width: 700px;
        margin: 0 auto;
        padding: 0 1em;
      }
    </style>
    <script defer src="https://cdn.jsdelivr.net/npm/a11y-dialog@7/dist/a11y-dialog.min.js"></script>
  </head>

  <body>
    <main>
      <div class="dialog-container" id="my-dialog" aria-hidden="true" aria-labelledby="my-dialog-title" role="dialog">
        <div class="dialog-overlay" data-a11y-dialog-hide></div>
        <div class="dialog-content" role="document">
          <button data-a11y-dialog-hide class="dialog-close" aria-label="Close this dialog window">
            ×
          </button>
          <a href="https://www.yahoo.com/" target="_blank">Rando Yahoo Link</a>
  
          <h1 id="my-dialog-title">My Title</h1>
          <p id="my-dialog-description">
            Some description of what's inside this dialog…
          </p>
        </div>
      </div>
      <button type="button" data-a11y-dialog-show="my-dialog">
        Open the dialog
      </button>
    </main>
    <script>
      // We need to ensure our deferred A11yDialog has
      // had a chance to do its thing ;-)
      window.addEventListener('DOMContentLoaded', (event) => {
        const dialogEl = document.getElementById('my-dialog')
        const dialog = new A11yDialog(dialogEl)
      });
    </script>
  </body>

</html>

I know, we’re ignoring a bunch of best practices (what, styles in the <head>?!) and combined all of the HTML, CSS, and JavaScript in one file. I won’t go into the details of the code as the focus here is testing for accessibility, but know that this test requires an internet connection as we are importing a11y-dialog from a CDN.

First, the manual checks

I served this one-pager locally and here are my manual check results:

FeatureResult
It should close when clicking outside the dialog (on the backdrop) or when pressing the ESC key.
It ought to trap focus to prevent tabbing out of the component with a keyboard.
It should allow forwarding tabbing with TAB and backward tabbing with SHIFT+TAB.
It should return focus back to the previously focused element when closed.
It should correctly apply aria-* attributes and toggles.
I verified this one “by eye” after inspecting the elements in the DevTools Elements panel.
It should provide Portals.Not applicable.
This is only useful when implementing the element with React, Svelte, Vue, etc. We’ve statically placed it on the page with aria-hidden for this test.
It should support for the alertdialog ARIA role for alert situations.
You’ll need to do two things:

First, remove data-a11y-dialog-hide from the overlay in the HTML so that it is <div class="dialog-overlay"></div>. Replace the dialog role with alertdialog so that it becomes:

<div class="dialog-container" id="my-dialog" aria-hidden="true" aria-labelledby="my-dialog-title" aria-describedby="my-dialog-description" role="alertdialog">

Now, clicking on the overlay outside of the dialog box does not close the dialog, as expected.
It should prevent the underlying body from scrolling, if needed.
I didn’t manually test but this, but it is clearly available per the documentation.
It should avoid the common pitfalls that come with the native <dialog> element.
This component does not rely on the native <dialog> which means we’re good here.

Next, let’s use some a11y tooling

I used Lighthouse to test the component both on a desktop computer and a mobile device, in two different scenarios where the dialog is open by default, and closed by default.

a11y-dialog Lighthouse testing, score 100.

I’ve found that sometimes the tooling doesn’t account for DOM elements that are dynamically shown or hidden DOM elements, so this test ensures I’m getting full coverage of both scenarios.

I also tested with IBM Equal Access Accessibility Checker. Generally, this tool will give you a red violation error if there’s anything egregious wrong. It will also ask you to manually review certain items. As seen here, there a couple of items for manual review, but no red violations.

a11y-dialog — tested with IBM Equal Access Accessibility Checker

Moving on to screen readers

Between my manual and tooling checks, I’m already feeling relatively confident that a11y-dialog is an accessible option for my dialog of choice. However, we ought to do our due diligence and consult a screen reader.

VoiceOver is the most convenient screen reader for me since I work on a Mac at the moment, but JAWS and NVDA are big names to look at as well. Like checking for UI consistency across browsers, it’s probably a good idea to test on more than one screen reader if you can.

VoiceOver caption over the a11y-modal example.

Here’s how I conducted the screen reader part of the audit with VoiceOver. Basically, I mapped out what actions needed testing and confirmed each one, like a script:

StepResult
The dialog component’s trigger button is announced.“Entering A11y Dialog Test, web content.”
The dialog should open when pressing CTRL+ALT +Space should show the dialog.“Dialog. Some description of what’s inside this dialog. You are currently on a dialog, inside of web content.”
The dialog should TAB to and put focus on the component’s Close button.“Close this dialog button. You are currently on a button, inside of web content.”
Tab to the link element and confirm it is announced.“Link, Rando Yahoo Link”
Pressing the SPACE key while focused on the Close button should close the dialog component and return to the last item in focus.

Testing with people

If you’re thinking we’re about to move on to testing with real people, I was unfortunately unable to find someone. If I had done this, though, I would have used a similar set of steps for them to run through while I observe, take notes, and ask a few questions about the general experience.

As you can see, a satisfactory audit involves a good deal of time and thought.

Fine, but I want to use a framework’s dialog component

That’s cool! Many frameworks have their own dialog component solution, so there’s lots to choose from. I don’t have some amazing spreadsheet audit of all the frameworks and libraries in the wild, and will spare you the work of evaluating them all.

Instead, here are some resources that might be good starting points and considerations for using a dialog component in some of the most widely used frameworks.

Disclaimer: I have not tested these personally. This is all stuff I found while researching.

Angular dialog options

In 2020, Deque published an article that audits Angular component libraries and the TL;DR was that Material (and its Angular/CDK library) and ngx-bootstrap both appear to provide decent dialog accessibility.

React dialog options

Reakit offers a dialog component that they claim is compliant with WAI-ARIA dialog guidelines, and chakra-ui appears to pay attention to its accessibility. Of course, Material is also available for React, so that’s worth a look as well. I’ve also heard good things about reach/dialog and Adobe’s @react-aria/dialog.

Vue dialog options

I’m a fan of Vuetensils, which is Austin Gil’s naked (aka headless) components library, which just so happens to have a dialog component. There’s also Vuetify, which is a popular Material implementation with a dialog of its own. I’ve also crossed paths with PrimeVue, but was surprised that its dialog component failed to return focus to the original element.

Svelte dialog options

You might want to look at svelte-headlessui. Material has a port in svelterial that is also worth a look. It seems that many current SvelteKit users prefer to build their own component sets as SvelteKit’s packaging idiom makes it super simple to do. If this is you, I would definitely recommend considering svelte-a11y-dialog as a convenient means to build custom dialogs, drawers, bottom sheets, etc.

I’ll also point out that my AgnosticUI library wraps the React, Vue, Svelte and Angular a11y-dialog adapter implementations we’ve been talking about earlier.

Bootstrap, of course

Bootstrap is still something many folks reach for, and unsurprisingly, it offers a dialog component. It requires you to follow some steps in order to make the modal accessible.

If you have other inclusive and accessible library-based dialog components that merit consideration, I’d love to know about them in the comments!

But I’m creating a custom design system

If you’re creating a design system or considering some other roll-your-own dialog approach, you can see just how many things need to be tested and taken into consideration… all for one component! It’s certainly doable to roll your own, of course, but I’d say it’s also extremely prone to error. You might ask yourself whether the effort is worthwhile when there are already battle-tested options to choose from.

I’ll simply leave you with something Scott O’Hara — co-editor of ARIA in HTML and HTML AAM specifications in addition to just being super helpful with all things accessibility — points out:

You could put in the effort to add in those extensions, or you could use a robust plugin like a11y-dialog and ensure that your dialogs will have a pretty consistent experience across all browsers.

Back to my objective…

I need that dialog to support React, Vue, Svelte, and Angular implementations.

I mentioned earlier that a11y-dialog already has ports for Vue and React. But the Vue port hasn’t yet been updated for Vue 3. Well, I was quite happy to spend the time I would have spent creating what likely would have been a buggy hand-rolled dialog component toward helping update the Vue port. I also added a Svelte port and one for Angular too. These are both very new and I would consider them experimental beta software at time of writing. Feedback welcome, of course!

It can support other components, too!

I think it’s worth pointing out that a dialog uses the same underlying concept for hiding and showing that can be used for a drawer (aka off-canvas) component. For example, if we borrow the CSS we used in our dialog accessibility audit and add a few additional classes, then a11y-dialog can be transformed into a working and effective drawer component:

.drawer-start { right: initial; }
.drawer-end { left: initial; }
.drawer-top { bottom: initial; }
.drawer-bottom { top: initial; }

.drawer-content {
  margin: initial;
  max-width: initial;
  width: 25rem;
  border-radius: initial;
}

.drawer-top .drawer-content,
.drawer-bottom .drawer-content {
  width: 100%;
}

These classes are used in an additive manner, essentially extending the base dialog component. This is exactly what I have started to do as I add my own drawer component to AgnosticUI. Saving time and reusing code FTW!

Wrapping up

Hopefully I’ve given you a good idea of the thinking process that goes into the making and maintenance of a component library. Could I have hand-rolled my own dialog component for the library? Absolutely! But I doubt it would have yielded better results than what a resource like Kitty’s a11y-dialog does, and the effort is daunting. There’s something cool about coming up with your own solution — and there may be good situations where you want to do that — but probably not at the cost of sacrificing something like accessibility.

Anyway, that’s how I arrived at my decision. I learned a lot about the native HTML <dialog> and its accessibility along the way, and I hope my journey gave you some of those nuggets too.


Dialog Components: Go Native HTML or Roll Your Own? originally published on CSS-Tricks. You should get the newsletter.

How to Implement and Style the Dialog Element

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

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

The post How to Implement and Style the Dialog Element appeared first on Codrops.

Choice Words about the Upcoming Deprecation of JavaScript Dialogs

It might be the very first thing a lot of people learn in JavaScript:

alert("Hello, World");

One day at CodePen, we woke up to a ton of customer support tickets about their Pens being broken, which ultimately boiled down to a version of Chrome that shipped where they ripped out alert() from functioning in cross-origin iframes. And all other native “JavaScript Dialogs” like confirm(), prompt() and I-don’t-know-what-else (onbeforeunload?, .htpasswd protected assets?).

Cross-origin iframes are essentially the heart of how CodePen works. You write code, and we execute it for you in an iframe that doesn’t share the same domain as CodePen itself, as the very first line of security defense. We didn’t hear any heads up or anything, but I’m sure the plans were on display.

I tweeted out of dismay. I get that there are potential security concerns here. JavaScript dialogs look the same whether they are triggered by an iframe or not, so apparently it’s confusing-at-best when they’re triggered by an iframe, particularly a cross-origin iframe where the parent page likely has little control. Well, outside of, ya know, a website like CodePen. Chrome cite performance concerns as well, as the nature of these JavaScript dialogs is that they block the main thread when open, which essentially halts everything.

There are all sorts of security and UX-annoyance issues that can come from iframes though. That’s why sandboxing is a thing. I can do this:

<iframe sandbox></iframe>

And that sucker is locked down. If some form tried to submit something in there: nope, won’t work. What if it tries to trigger a download? Nope. Ask for device access? No way. It can’t even load any JavaScript at all. That is unless I let it:

<iframe sandbox="allow-scripts allow-downloads ...etc"></iframe>

So why not an attribute for JavaScript dialogs? Ironically, there already is one: allow-modals. I’m not entirely sure why that isn’t good enough, but as I understand it, nuking JavaScript dialogs in cross-origin iframes is just a stepping stone on the ultimate goal: removing them from the web platform entirely.

Daaaaaang. Entirely? That’s the word. Imagine the number of programming tutorials that will just be outright broken.

For now, even the cross-origin removal is delayed until January 2022, but as far as we know this is going to proceed, and then subsequent steps will happen to remove them entirely. This is spearheaded by Chrome, but the status reports that both Firefox and Safari are on board with the change. Plus, this is a specced change, so I guess we can waggle our fingers literally everywhere here, if you, like me, feel like this wasn’t particularly well-handled.

What we’ve been told so far, the solution is to use postMessage if you really absolutely need to keep this functionality for cross-origin iframes. That sends the string the user uses in window.alert up to the parent page and triggers the alert from there. I’m not the biggest fan here, because:

  1. postMessage is not blocking like JavaScript dialogs are. This changes application flow.
  2. I have to inject code into users code for this. This is new technical debt and it can harm the expectations of expected user output (e.g. an extra <script> in their HTML has weird implications, like changing what :nth-child and friends select).
  3. I’m generally concerned about passing anything user-generated to a parent to execute. I’m sure there are theoretical ways to do it safely, but XSS attack vectors are always surprising in their ingenouity.

Even lower-key suggestions, like window.alert = console.log, have essentially the same issues.

Allow me to hand the mic over to others for their opinions.

Couldn’t the alert be contained to the iframe instead of showing up in the parent window?

Jaden Baptista, Twitter

Yes, please! Doesn’t that solve a big part of this? While making the UX of these dialogs more useful? Put the dang dialogs inside the <iframe>.

“Don’t break the web.” to “Don’t break 90% of the web.” and now “Don’t break the web whose content we agree with.”

Matthew Phillips, Twitter

I respect the desire to get rid of inelegant parts [of the HTML spec] that can be seen as historical mistakes and that cause implementation complexity, but I can’t shake the feeling that the existing use cases are treated with very little respect or curiosity.

Dan Abramov, Twitter

It’s weird to me this is part of the HTML spec, not the JavaScript spec. Right?!

I always thought there was a sort of “prime directive” not to break the web? I’ve literally seen web-based games that used alert as a “pause”, leveraging the blocking nature as a feature. Like: <button onclick="alert('paused')">Pause</button>[.] Funny, but true.

Ben Lesh, Twitter

A metric was cited that only 0.006% of all page views contain a cross-origin iframe that uses these functions, yet:

Seems like a misleading metric for something like confirm(). E.g. if account deletion flow is using confirm() and breaks because of a change to it, this doesn’t mean account deletion flow wasn’t important. It just means people don’t hit it on every session.

Dan Abramov, Twitter

That’s what’s extra concerning to me: alert() is one thing, but confirm() literally returns true or false, meaning it is a logical control structure in a program. Removing that breaks websites, no question. Chris Ferdinandi showed me this little obscure website that uses it:

Speaking of Chris:

The condescending “did you actually read it, it’s so clear” refrain is patronizing AF. It’s the equivalent of “just” or “simply” in developer documentation.

I read it. I didn’t understand it. That’s why I asked someone whose literal job is communicating with developers about changes Chrome makes to the platform.

This is not isolated to one developer at Chrome. The entire message thread where this change was surfaced is filled with folks begging Chrome not to move forward with this proposal because it will break all-the-things.

Chris Ferdinandi, “Google vs. the web”

And here’s Jeremy:

[…] breaking changes don’t happen often on the web. They are—and should be—rare. If that were to change, the web would suffer massively in terms of predictability.

Secondly, the onus is not on web developers to keep track of older features in danger of being deprecated. That’s on the browser makers. I sincerely hope we’re not expected to consult a site called canistilluse.com.

Jeremy Keith, “Foundations”

I’ve painted a pretty bleak picture here. To be fair, there were some tweets with the Yes!! Finally!! vibe, but they didn’t feel like critical assessments to me as much as random Google cheerleading.

Believe it or not, I generally am a fan of Google and think they do a good job of pushing the web forward. I also think it’s appropriate to waggle fingers when I see problems and request they do better. “Better” here means way more developer and user outreach to spell out the situation, way more conversation about the potential implications and transition ideas, and way more openness to bending the course ahead.


The post Choice Words about the Upcoming Deprecation of JavaScript Dialogs appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

Some Hands-On with the HTML Dialog Element

This is me looking at the HTML <dialog> element for the first time. I've been aware of it for a while, but haven't taken it for a spin yet. It has some pretty cool and compelling features. I can't decide for you if you should use it in production on your sites, but I'd think it's starting to be possible.

It's not just a semantic element, it has APIs and special CSS.

We'll get to that stuff in a moment, but it's notable because it makes the browser support stuff significant.

When we first got HTML5 elements like <article>, it pretty much didn't matter if the browser supported it or not because nothing was worse-off in those scenarios if you used it. You could make it block-level and it was just like a meaningless div you would have used anyway.

That said, I wouldn't just use <dialog> as a "more semantic <div> replacement." It's got too much functionality for that.

Let's do the browser support thing.

As I write:

  • Chrome's got it (37+), so Edge is about to get it.
  • Firefox has the User-Agent (UA) styles in place (69+), but the functionality is behind a dom.dialog_element.enabled flag. Even with the flag, it doesn't look like we get the CSS stuff yet.
  • No support from Safari.

It's polyfillable.

It's certainly more compelling to use features with a better support than this, but I'd say it's close and it might just cross the line if you're the polyfilling type anyway.

On to the juicy stuff.

A dialog is either open or not.

<dialog>
  I'm closed.
</dialog>

<dialog open>
  I'm open.
</dialog>

There is some UA styling to them.

It seems to match in Chrome and Firefox. The UA stylesheet has it centered with auto margins, thick black lines, sized to content.

See the Pen
Basic Open Dialog
by Chris Coyier (@chriscoyier)
on CodePen.

Like any UA styles, you'll almost surely override them with your own fancy dialog styles — shadows and typography and whatever else matches your site's style.

There is a JavaScript API for opening and closing them.

Say you have a reference to the element named dialog:

dialog.show();
dialog.hide();

See the Pen
Basic Togglable Dialog
by Chris Coyier (@chriscoyier)
on CodePen.

You should probably use this more explicit command though:

dialog.showModal();

That's what makes the backdrop work (and we'll get to that soon). I'm not sure I quite grok it, but the the spec talks about a "pending dialog stack" and this API will open the modal pending that stack. Here's a modal that can open a second stacking modal:

See the Pen
Basic Open Dialog
by Chris Coyier (@chriscoyier)
on CodePen.

There's also an HTML-based way close them.

If you put a special kind of form in there, submitting the form will close the modal.

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

See the Pen
Dialog with Form that Closes Dialog
by Chris Coyier (@chriscoyier)
on CodePen.

Notice that if you programmatically open the dialog, you get a backdrop cover.

This has always been one of the more finicky things about building your own dialogs. A common UI pattern is to darken the background behind the dialog to focus attention on the dialog.

We get that for free with <dialog>, assuming you open it via JavaScript. You control the look of it with the ::backdrop pseudo-element. Instead of the low-opacity black default, let's do red with stripes:

See the Pen
Custom Dialog Backdrop
by Chris Coyier (@chriscoyier)
on CodePen.

Cool bonus: you can use backdrop-filter to do stuff like blur the background.

See the Pen
Native Dialog
by Chris Coyier (@chriscoyier)
on CodePen.

It moves focus for you

I don't know much about this stuff, but I can fire up VoiceOver on my Mac and see the dialog come into focus see that when I trigger the button that opens the modal.

It doesn't trap focus there, and I hear that's ideal for modals. We have a clever idea for that utilizing CSS you could explore.

Rob Dodson said: "modals are actually the boss battle at the end of web accessibility." Kinda nice that the native browser version helps with a lot of that. You even automatically get the Escape key closing functionality, which is great. There's no click outside to close, though. Perhaps someday pending user feedback.

Ire's article is a go-to resource for building your own dialogs and a ton of accessibility considerations when using them.

The post Some Hands-On with the HTML Dialog Element appeared first on CSS-Tricks.