-
Notifications
You must be signed in to change notification settings - Fork 2
Description
Baklava is now making extensive use of top layer elements (dialog and popover) for things like modals, tooltips, and toast notifications. With modern browser support this is working pretty well, but there are still some limitations.
Issue 1: There is no foolproof way (with either JS or CSS) to detect that an element is currently in the top layer. You can mostly do this with the :modal
/popover-open
pseudo-classes, however this breaks down when you introduce exit animations. I'll use <dialog>
as an example. During the exit animation, a <dialog>
element will no longer be in :modal
state, and it won't have the open
attribute anymore either. There is no way to tell that the dialog is actually still in the top layer for styling purposes.
As a workaround, you can apply the styling (positioning etc.) just on the base dialog
selector. This works, but it means we cannot generally apply default styling of dialog
elements without knowing whether it's going to be used as a modal or not. Maybe we can assume aria-model="true"
is set on modal dialogs as an indicator that it is intended to be used as a modal? See the reset code here for workarounds/notes:
baklava/src/styling/global/reset.scss
Line 75 in 7f23a79
Note: the `:modal` pseudo-class applies when the modal is in the top layer *and* currently open. That means that it |
In Chrome's author styling they use a :-internal-dialog-in-top-layer
pseudo class for this, but this is not yet standardized.
Note: this also applies to popovers (with :popover-open
instead of :modal
), although there it's not as much of a problem because all elements that are intended to be used as popovers must explicitly get a popover
attribute, which we can then target for styling.
Links:
- [css-position] User-agent styles for top layer transitions w3c/csswg-drafts#9912 - "User-agent styles for top layer transitions"
- Fix user-agent style rules for top layer transitions whatwg/html#9387 (comment)
- https://issues.chromium.org/issues/40270744 -
:-internal-dialog-in-top-layer
Issue 2: There is no (easy) way to change the ordering of top layer elements. They are always displayed in stack order of activation. In addition, there is not even a way to programmatically read the elements that are currently in the top layer (e.g. through a DOM API).
This can be a problem if we want some elements to always be displayed above the topmost layer, for example global toast notifications that should not be obscured by a modal. See WHATWG discussions here and here, and ecosystem discussions for react-toastify, mui.
A possible workaround is to detect when the top layer changes and then re-triggering showPopover()
on the global elements so they remain in the topmost layer. However, this runs into another issue where these global elements will not be interactive if there is a modal open (see issue 3 below).
Issue 3: Popovers not inside the topmost modal are not interactive.
The way modal dialogs work, only content that is nested inside the <dialog>
will be interactive, everything else is considered inert. This is a problem if we want to open some popover that is not in that dialog, for example toast notifications which should be interactive and are generally rendered from element high in the DOM.
Discussion:
- Should non-modal top layer elements that come after modal dialogs also escape inertness? whatwg/html#10811
- A popover on top of a modal dialog should be interactable whatwg/html#9936
- Demo 1: https://jsbin.com/mugebod/1/edit?html,output
- Demo 2 (toast): https://codepen.io/argyleink/pen/gbYpNLY/a533152314a2ce88bb6ae830f48e032b
A possible workaround is here using MutationObserver
to detect when things are added to/removed from the top layer and using React's createPortal()
to portal global popovers to the topmost modal.
There is a proposed interactivity
CSS property that could solve this in the future by allowing certain elements to "escape" inertness.
Some other (minor) issues:
-
There is no easy way to track dialog elements that open. Unlike with closing where we have the
close
event, there is equivalent "open" event yet. This is coming soon however with thebeforetoggle
andtoggle
events. Coming in Chrome v132. -
"Light dismiss", like clicking on the backdrop, to close dialog elements is not (yet) supported natively. A
<dialog closedby/>
attribute is being implemented. There are also some pretty easy workarounds. -
There is no foolproof way to prevent a dialog from being closed by users, see here for motivation. When the user requests the modal to close (for example through the Escape key), a
cancel
event is fired, and this event is cancelable through.preventDefault()
. However, at least in Chrome users can force the close to happen by pressing Escape twice. Looks like this will be resolved withclosedby="none"
in the near future. Incidentally,role="alertdialog"
should apparently also disable Escape according to the spec? -
Non-modal dialogs (
<dialog>
withopen
attribute set, or through JS withshow()
rather thanshowModal()
) are a bit problematic. Chrome (at least v131) will explicitly not render these elements in the top layer, even if we useshowPopover()
. See this issue. As a workaround, we should avoid settingopen
unless it's throughshowModal()
.- What if we want to show a
<dialog>
inline on the page? We could perhaps just set:not([open]) { display: block; }
. But would this give issues with accessibility? How would this look in the accessibility tree? Do user agents explicitly look at theopen
attribute for semantics? Might be better to only ever use<dialog>
for modals (showModal()
) and popovers (showPopover())
.
- What if we want to show a
And a few issues that are now widely fixed in browsers:
-
In some older browser versions (as per an older version of the spec), modals cannot be nested inside popovers. See ticket: Modals cannot be nested in popovers in some browsers #87. Seems fixed in all browsers, including Safari as of at least v18.2.
-
Previously,
::backdrop
pseudo-elements would not inherit any CSS properties, which meant that custom properties were not available. This is now fixed in all major browsers, see: https://developer.chrome.com/blog/css-backdrop-inheritance -
Focus management: according to the latest spec, browsers should (1) focus the first focusable element in a dialog upon opening (or
autofocus
if present, or the dialog itself is no focusable items), and (2) should re-focus the last focused element upon dialog close. This seems to be widely implemented in browsers now, but wasn't always the case.
Further reading: