DEV Community

Cover image for Dialogs, Popovers & the Top Layer Mess
Andrew Bone
Andrew Bone

Posted on • Edited on

Dialogs, Popovers & the Top Layer Mess

Dialogs (or modals) and popovers are essential UI components that interrupt the user's workflow to display critical information or offer interactive elements.

A dialog is a UI element that demands user interaction before proceeding. It is commonly used for confirmations, forms, and critical alerts.

A popover is a lightweight floating UI element that provides contextual actions or extra information, such as dropdowns, tooltips, notifications, and toasts.

Both components help manage interactions without navigating away from the page.

The Problem

The main challenge when using modals and popovers together is managing their stacking order. When a modal is opened while a popover is visible, the popover often gets hidden behind the modal and becomes inert. Even if reopened, the popover stays non-interactive despite appearing above the modal, making it a frustrating issue for users and developers.

GitHub Issues Timeline

  • Issue #9075 (March 2023): OddBird proposes a way to retrieve the current top layer order.
  • Issue #9733 (September 2023): keithamus suggests an event to notify when modals or popovers appear.

Real-World Examples

Imagine you have a piece of software that you maintain. You're planning on doing some server maintenance and so place a banner at the top of the screen letting people know about it. A lot of the interactions on your site take place in modals. Suddenly, your banner is hidden!

That's ok though because you're a savvy developer that knows all about the new popover API. Using this, you move the banner to the top layer and place it above the modal, only, it's no longer interactive.

The only way to keep it interactive is to place the banner inside the topmost modal. While this works, it’s cumbersome and impractical, especially without a built-in way to track the top layer.

Here’s an example where you can open multiple dialogs and popovers. Click "Start" to open a modal dialog, then open a popover. You’ll notice the popover becomes inert even though it was opened after the dialog. You can continue to open more dialogs and see the popover fade further and further into the background.

What Can We Do About It?

There are a few ways we can tackle this, but I'll focus on two: updating the spec and tracking the top layer ourselves.

Updating the Spec

As the web is open, we can contribute to improving standards. Proposals for changes to the specification can be submitted to the WHATWG HTML repository. Ensure your suggestion is thoroughly considered to facilitate the process.

A solution I'd like to see is a set of methods and properties added to the document allowing us to interact with the top layer, an event fired when the top layer order changes, and finally, I'd love modals to only inert items lower than themselves in the top layer.

Methods and Properties

  • document.topLayer.elements: Returns an array of DOM elements where the index relates to position in the top layer.
  • document.topLayer.bringToFront(el: HTMLElement): Takes an element in the top layer and moves it to the last/highest position.

Events

  • toplayerchange: An event fired on document.topLayer when the top layer has an element added, removed, or reordered. The event contains an enum specifying the change type.

Inert

I cannot envisage a scenario where you have a popover visually above a modal (due to top layer positioning) that you want to be inert. This feels like a spec oversight that can be addressed.

Handling It Ourselves

One of the issues mentioned earlier was recently resolved, introducing a new event that lets us track top-layer changes. Using this, we can create a polyfill to manage element layering (though fixing the inert issue still requires additional work).

The event we've gained is the toggle event. This fires when a popover, dialog, or details element is toggled. We care about popover and specifically modal dialogs, meaning this event is a little overkill but it'll get the job done.

document.addEventListener(
  "toggle",
  ({ target }) => {
    if (!(target instanceof HTMLDialogElement || target.hasAttribute("popover"))) return;

    if (target.matches(":modal, :popover-open") && document.contains(target)) {
      console.log('toplayerchanged: add')
    } else {
      console.log('toplayerchanged: remove')
    }
  },
  { capture: true }
);
Enter fullscreen mode Exit fullscreen mode

Using this, we can detect when elements are added or removed from the top layer. However, we still need to store them somewhere and ensure elements are removed if they are deleted from the DOM without firing a toggle event.

const topLayerElements = new Set();

document.addEventListener(
  "toggle",
  ({ target }) => {
    if (!(target instanceof HTMLDialogElement || target.hasAttribute("popover"))) return;

    if (target.matches(":modal, :popover-open") && document.contains(target)) {
      topLayerElements.add(target);
      document.dispatchEvent(
        new CustomEvent("toplayerchanged", { detail: "add" })
      );
    } else {
      if (topLayerElements.delete(target)) {
        document.dispatchEvent(
          new CustomEvent("toplayerchanged", { detail: "remove" })
        );
      }
    }
  },
  { capture: true }
);

const observer = new MutationObserver((mutations) => {
  const nodes = mutations.flatMap(({ removedNodes }) => [...removedNodes]);

  for (const node of nodes)
    if (topLayerElements.delete(node)) {
      document.dispatchEvent(
        new CustomEvent("toplayerchanged", { detail: "remove" })
      );
    }
});

observer.observe(document.body, { childList: true, subtree: true });

document.getTopLayerElements = () => [...topLayerElements];
Enter fullscreen mode Exit fullscreen mode

With this, we can run document.getTopLayerElements() to get a list of items in the top layer and listen for a toplayerchanged event on the document to know when the list changes.

Example React Hook

I've created a React Hook that uses the above method to track the top layer. With this information, you can portal and re-promote popovers to help prevent them from getting stuck behind modals.

Check it out here: React Hook for Top Layer Stack Management

Conclusion

These stacking issues make dialogs harder to use effectively, limiting their full potential. By addressing problems like this, we can improve the web and create a more user-friendly experience for everyone. If anyone has any thoughts, ideas, or just wants to talk about the problem more, feel free to leave a comment.

Thanks for reading! If you'd like to connect, here are my Twitter, BlueSky, and LinkedIn profiles. Come say hi 😊

Top comments (1)