whatwg / html

HTML Standard
https://html.spec.whatwg.org/multipage/
Other
8.01k stars 2.62k forks source link

A popover on top of a modal dialog should be interactable #9936

Open benfrain opened 10 months ago

benfrain commented 10 months ago

Here is a write up of the issue: https://benfrain.com/failing-with-multiple-dialog-elements-understanding-the-top-layer-and-popovers/

And from that a simple reduction of what I feel is an issue: https://benfrain.com/playground/modals/popover/

I'd opened a bug originally on the Chromium bug tracker: https://bugs.chromium.org/p/chromium/issues/detail?id=1502869

Following that and the related bug https://bugs.chromium.org/p/chromium/issues/detail?id=1502133 I am here.

I feel it is counterintuitive for authors to have popovers in the top layer, appearing above a modal dialog but for that popover to be impossible to interact with.

If a popover is appearing on top of a dialog, in this case by design, it stands to reason (to me anway) one might want to interact with it.

nt1m commented 10 months ago

This is indeed the current spec behavior: https://html.spec.whatwg.org/multipage/interaction.html#blocked-by-a-modal-dialog

Perhaps the definition could be tweaked to not block elements on above the modal dialog in the top layer.

npenin commented 9 months ago

Just to add on that, the MDN web docs states the following (https://developer.mozilla.org/en-US/docs/Web/API/Popover_API):

Typical use cases for the popover API include user-interactive elements like action menus, custom "toast" notifications, form element suggestions, content pickers, or teaching UI.

That means that toast notifications, menus, ... are not interactable when a modal is open. And potentially even hidden because of the https://html.spec.whatwg.org/multipage/popover.html#show-popover algorithm, responsible for hiding all popovers.

askvortsov1 commented 7 months ago

I also ran into this. Another argument for changing the current behavior+spec is that sibling dialog + popover (code) vs nested dialog > popover (code) stack the same way, but in the former, the popover is inert even though it covers the modal. I would expect these two to behave consistently.

Perhaps the definition could be tweaked to not block elements on above the modal dialog in the top layer.

This seems reasonable. If A is displayed on top of B, and B is interactive, I would expect A to also be interactive, and for interactions on the intersection of A and B to go to A.

scottaohara commented 6 months ago

i'm torn on this. because it'd be a really unexpected change in established behavior that a modal dialog can have content accessible outside of it. That essentially breaks the whole concept of a modal being modal (the content of the primary page being inert) - particularly in the case where content being revealed from a control in the modal dialog, but focus doesn't automatically move to it.... so why would anyone using a screen reader, for instance, even think that now there's content outside of the dialog that they can interact with?

For many related use cases that I've been made aware of, I would often end up suggesting that what "should" have been done is the additional popups/overs be descendants of the modal dialog, so a user (specifically user of AT) continues to remain in the context of the modal dialog - which is again, what has been the established expectation/promise of what a modal dialog represents - so the content that popups 'on top' of the dialog is actually accessible.

With that said, I acknowledge problems that exist where one might have global notifications that live outside of the modal dialog, and may be triggered due to an action within the dialog. These sorts of things do need to be exposed to users. The solution I've had to work with developers to implement is to essentially have modal dialogs have their own instance of notifications, which i understand is not ideal. Ideally the notification API proposal could help solve for some of these cases, but if a notification has a nested interactive element (e.g., a link) - well, that's just a whole separate issue that i'll not dive into here (yet?)

I just think it's really important to consider the ramifications of the ask - where there are definitely some use cases which would need this revised behavior. But I worry the simplification of "if a popover is displayed on top of a modal dialog, it should be interactable" glosses over the "well why is this UI built this way / does it need to be build this way?" and "why would someone invoke a modal dialog but also expect for people to interact with content that isn't part of that modal dialog, since the point of a modal is to make everything outside of it inert?" It seems to be coming from a place of "visually this makes sense" but it goes against what the developer has built with their markup. Again, i acknowledge there are some use cases that should be considered here, but the reduced test cases in this thread are too generic to draw any strong conclusions on as to whether the use situations that these reduced cases derived from are actually valid for consideration or not. The position of content in the DOM (and thus how it is exposed in the accessibility tree by browsers) is important to not make unpredictable.

npenin commented 6 months ago

Really interesting insight ! I would actually not put such a strong condition on the title of this issue. In the example below, the popover would not be "on top" of the popup, but just aside from it : The typical use case I can imagine would be an error snackbar that is "popping up" after a modal dialog action (like api interaction). If action fails for whatever reason, we would want to display the snackbar and let the user interact with it for a retry operation, close it or just report the problem to the technical support.

I actually really like the way chrome did it: having a top layer. What about introducing a similar layer concept and let web developers handle the layer ordering ? or just defining a predefined set of layers that each "layerable" content can decide to use (showModal could take an optional argument for the layer name/index, similar rule would apply to other "layerable" content like the showPopover, ...). In that case those layers would be more deterministic and might help providing guidance on which layer to use when.

thejackshelton commented 5 months ago

A good use case I could really see for this is portals. Imagine you have a form with a headless select component inside a modal dialog. That select component will "portal" its contents outside of your top layer, and since you don't control the rendering, it will remain behind the modal dialog. (also remain inert)

By setting a popover attribute, it should be promoted to the top layer and be interactable. That way it's backwards compatible to some degree, and is also opt-in.

keithamus commented 5 months ago

@thejackshelton in that instance the popover is inside the dialog and therefore inter-actable.

Perhaps, another way to look at this, is to question why the popover is being opened in the first place, and what that does to the document? Perhaps one solution is that showing a popover outside of the modal should close the modal itself? I know this goes against the problem space presented in the OP but it makes the authoring error much more visible.

thejackshelton commented 5 months ago

@thejackshelton in that instance the popover is inside the dialog and therefore inter-actable.

Perhaps, another way to look at this, is to question why the popover is being opened in the first place, and what that does to the document? Perhaps one solution is that showing a popover outside of the modal should close the modal itself? I know this goes against the problem space presented in the OP but it makes the authoring error much more visible.

While the component or trigger itself might be in the dialog, its contents are "moved" out of the top layer of the modal dialog (or were never there, and were conditionally rendered to the end of the body).

As a result, you can't currently use libraries that portal their contents in a dialog element.

Example I've found in the wild: https://www.linkedin.com/posts/joshwcomeau_ive-been-experimenting-with-the-native-activity-7153416928533835776-gRwW/

An example we have in Qwik UI: https://github.com/qwikifiers/qwik-ui/issues/694

My current thoughts, are though you cannot control the rendering of the portalled content, perhaps you can programmatically make it a popover, and when that happens the content that was originally meant to be inside the dialog is interactable.

Another solution I have tried, is a mutation observer that moves it back into the dialog, which unfortunately seems to break most of these libraries.

keithamus commented 5 months ago

As a result, you can't currently use libraries that portal their contents in a dialog element.

Those libraries should probably update to either not use portals, inject their portal root into the nearest dialog, or to make the portals popovers themselves. Personally I don't see much utility in portals given popover effectively supersedes them.

jods4 commented 5 months ago

Sure, components whose lifetime are tied to the dialog can evolve to techniques other than a Portal at the end of <body>. I have implemented dropdowns and tooltips inside dialogs, it's possible. And with new popover APIs there are additional options that may even be easier.

The problem still remains for components that should outlive the dialog. Toasters/notifications have been mentioned before and are the use-case that led me to this issue.

Assuming you managed to even "display" your notifications on top, which is not easy. You need to either work around the ::backdrop, or popover the notifications container every time a dialog opens.

It's almost like we need a "top-most" layer for content that's always on top of everything else, including the top layer 😄

thejackshelton commented 5 months ago

Those libraries should probably update to either not use portals, inject their portal root into the nearest dialog, or to make the portals popovers themselves. Personally I don't see much utility in portals given popover effectively supersedes them.

While I agree, it is such a common pattern that I think it will be quite a while before that becomes a reality. As a result, you can't effectively use these libraries in dialogs at the moment.

For the time being, would it be accessible to have a custom aria widget with a custom backdrop that is a "inert" piece, and then promote that into the top layer as a popover instead?

thejackshelton commented 5 months ago

Sure, components whose lifetime are tied to the dialog can evolve to techniques other than a Portal at the end of <body>. I have implemented dropdowns and tooltips inside dialogs, it's possible. And with new popover APIs there are additional options that may even be easier.

The problem still remains for components that should outlive the dialog. Toasters/notifications have been mentioned before and are the use-case that led me to this issue.

  • it's a common UX pattern to display notifications to user, even when a dialog is open.
  • when dialog closes, you don't want all notifications to suddenly disappear, so parenting inside the dialog seems wrong.
  • if you have interactions in your notifications (even just "click to dismiss") they don't work when a dialog is open.

Assuming you managed to even "display" your notifications on top, which is not easy. You need to either work around the ::backdrop, or popover the notifications container every time a dialog opens.

It's almost like we need a "top-most" layer for content that's always on top of everything else, including the top layer 😄

It is definitely possible, we are doing it in Qwik UI. What I am saying is that for example if someone uses the Qwik UI modal which uses the dialog element under the hood, and then they decide to use a headless select component, that is not going to work. (as most of these portal it outside of the top layer)

Now it would work if they use a Qwik UI date picker for example, but then they cannot leverage the previous ecosystem (across everything). It is a breaking change for the web.

keithamus commented 5 months ago

For the time being, would it be accessible to have a custom aria widget with a custom backdrop that is a "inert" piece, and then promote that into the top layer as a popover instead?

I think it would be an easier lift to move portals to using popover than to re-implement <dialog>.

thejackshelton commented 5 months ago

For the time being, would it be accessible to have a custom aria widget with a custom backdrop that is a "inert" piece, and then promote that into the top layer as a popover instead?

I think it would be an easier lift to move portals to using popover than to re-implement <dialog>.

If I had control over the rendering of the library completely agree.

As a library author making use of the dialog we don't, the portalled content moves outside of the dialog, and isn't interactive, even if it is visually when programmatically made a popover, as per this issue. I would classify this as a bug, but perhaps that is a matter of opinion seen from the discussion above.

While I'd love to get rid of portals, there are thousands of libraries that use them. In this case, we have a Modal component in Qwik UI that builds on top of the native dialog element. Should I be informing those who consume the component that the respective library should always move to a popover or else it can't be used?

Now my second thought was using a portal implementation like the other libraries and not using the popover, but then you get the same problem in reverse. Libraries that now use the dialog or popover will take precedence because of the top layer.

askvortsov1 commented 4 months ago

In the example below, the popover would not be "on top" of the popup, but just aside from it

In terms of DOM layout, yes. But the current toplayer spec doesn't allow 2 elements in the toplayer to be "besides" each other; things are stacked in the order in which they were opened.

could take an optional argument for the layer name/index

I think it would be sad if the toplayer became z-index v2. I'm not opposed to more nuanced semantics than "elements are stacked in the order they were opened", but I feel like allowing developers to explicitly dictate the order of toplayer elements would significantly weaken the assumptions you can make when writing / using components, just as a blanket "exempt me from inertness" would weaken inertness.


Portalling toplayer elements has an advantage outside of making sure they don't get clipped by overflow/contain: it allows you to add a tooltip / popover anchored toany element without disrupting CSS selectors for your app. For instance:

<html lang="en">
  <body>
    <ul>
      <li>Hi</li>
      <li>I'm</li>
      <li>A</li>
      <dialog>This is a modal</dialog>
      <li>List</li>
    </ul>
  </body>
  <style>
    li:nth-child(2n) {
      color: red;
    }
    li:nth-child(2n + 1) {
      color: green;
    }
  </style>
</html>

https://fish-auspicious-composer.glitch.me/

Of course, this particular <dialog /> could be placed somewhere where it doesn't interfere with these particular selectors. But it will always interfere with some selectors. Suddenly, you need to think about where to place every toplayer element, or accept the uncertainty of "some one could wrap my component in a tooltip, and mess up my css". display: contents; gets us fairly far w.r.t. the stylings themselves, but selectors are still going to be disrupted.

We don't want to blanket-exclude it from selectors while not visible either; that would likely break animating closing / opening it, and feels like a pretty radical deviation from the rules of the DOM we are used to.

If you portal toplayer elements outside the root of your app's DOM, you don't need to worry about this. Global styles can still be set via :root, and global event listeners can still be attached to the document element / window. And if you can use portalling, you're probably using enough JS that you can dynamically specify classes / other elements for each thing you're portalling, so you don't lose power there.

Additionally, if your DOM structure consists of (1) your app root, and (2) a bunch of portalled toplayer elements, it becomes really easy to implement the modal behavior proposed in above via popovers by:


This all being said, I still think we should change the spec, although I'm less certain exactly how. Here are some thoughts.

Global notifications are a thing

Sometimes, it's easy to put a locally-triggered popover inside of a modal. If the popover is anchored to something in the modal, this seems like the obviously correct decision. And if a popover is only triggerable by actions from inside the modal, it's probably reasonable to put it inside the modal too. But there are other cases:

And so unless you move all your popovers every time any modal opens or closes, it's difficult to keep them inside the modal. It also doesn't feel "correct": globally-relevant elements should probably live somewhere, uh, global.

There are multiple kinds of modals

A question I've been thinking about a lot in relation to this issue is "Why does this have to be a modal?”

A big benefit of modals over separate pages is that they feel less disruptive to a user's interaction. You're navigating away, with the potential to return. You're stepping aside to do something heavily related to the page you're currently on, and will definitely return to your main task when done.

A benefit of modals over regular popovers is that forcibly contain you within a subview. You still have context behind the modal, but you can't interact with it until you're finished doing whatever you set out to do. Some benefits of this are:

But I claim that there are different degrees to which you might want to constrain the user.

The "share" dialog on Google Drive is pleasant UI: the user needs to do a task in the context of their document, so a modal is used to let them complete the task and then finish. But if you disconnect from the server, or get a notification, you might want/need to do something about it, interrupting your current task. And maybe that includes clicking an "info" link in a popup. Or closing a notification, then closing the dialog and dealing with something.

In contrast, if you need to enforce a paywall, or an age verification check (e.g. on alcohol-related purchases in the U.S.), you might want a non-interruptible modal, that forces the user to complete the task before dealing with anything extraneous. I see this category kinda like more powerful, customizable "alert"/"confirm"s.

I think we should change something

I claim that interruptible and non-interruptible modals are both useful components in a UI toolkit.

I would love to see native support for interruptible modals (i.e. popovers placed above these escape inertness). This is the implementation I described above with portalling + manual inertness, but it relies on a bunch of custom JS / tracking open / closed state + order, and on all popovers being portalled. If any end up inside the app root, they will be inert. And if one of these pseudo-modals is placed inside the app root, opening it will make everything inert with no escape.

I am less confident on how "non-interruptible" modals should be handled. <dialog /> today is pretty close to this visually / behaviorally, and almost exactly this from an accessibility standpoint. I have a few ideas on how to improve it further, none of which I love:

  1. Non-descendant popovers opened while a <dialog /> is open should be placed under that <dialog /> in the toplayer.
  2. showPopover should raise an exception if called while a <dialog /> is open
  3. <dialog />s should somehow pause execution of the main JS thread while they are open

(3) seems impossible to implement, especially with frameworks that have runtimes, but it feels like the most "correct" option in that it would be a stylable alert/confirm.

(2) feels unpleasant: it adds a lot of places where exceptions need to be accounted for, and would be a very breaking change.

(1) is my favorite of these, but it does weaken the definition of the toplayer somewhat. I lean positively towards this being worth it, but I think more discussion is warranted.

achshar commented 3 months ago

As I understand it, allowing the popover element to be intractable sends us back to square one where we then need z-index within the top-layer itself. The entire purpose of the dialog element is defeated.

Having said that, one proposed solution can be as follows:

The second situation also ensures that the popover element necessarily opened after the dialog was opened. Which would be in line with the user's expectations that what was shown later is over on top of what was shown earlier. I believe this hits a compromise between the two ends and solves a few of the use-cases discussed above.

On a personal note: I strongly feel that having popover and dialog in the same layer is a no-win situation, we seem to be digressing in the earlier state in terms of layering elements on top of one another.

askvortsov1 commented 3 months ago

If the popover element is not a descendent of the dialog element - Show the popover but let it not be interactive. (So keep it as it is) If however the popover element is a descendent of the dialog element - Make the popover interactive.

Isn't this how things work right now?

I think that dialogs opened after a popover currently work correctly: the popover will be inert, and will be under the dialog in the toplayer. The thing I am sad about is that if you open a dialog and then open a non-descendant popover, the popover will be placed on top of the dialog, visually obscuring it, but it will be inert.

having popover and dialog in the same layer is a no-win situation I agree with this, but I don't think anyone is proposing it. My proposal is that:

If you open a dialog, and then open a non-descendant popover, the popover should be put on its own layer, but that layer should be below the dialog

At some point in the future, we might also want to consider some notion of "global notification" elements, which will be exert from inertness, and be placed above all other toplayer components.

oliviertassinari commented 2 months ago

Could this problem also be why we can't use nested modal <dialog>? https://jsbin.com/meqobim/edit?html,js,output

  <dialog id="dlg">
    <form method="dialog">
      <input />
      <button id="btn2">Open dialog 2</button>
      <input />
      <button type="submit">Close</button>
    </form>
    <dialog id="dlg2">
      <form method="dialog">
        <input />
        <input />
        <button type="submit">Close</button>
      </form>
    </dialog>
  </dialog>
clshortfuse commented 2 months ago

Hitting this with an combobox inside a native dialog. The alternative is to remove the native HTMLDialogElement which hurts accessibility. Ironically enough, the combobox works perfectly fine with ARIA tags because it's reporting aria-expanded and all the related listbox ARIA tags correctly with keyboard input.

But because the popover can't be visually interacted with by mouse/touch we'd have to break the ARIA accessibility in favor of supporting mouse/touch operations.

Edit: Apologies. I took a second look at the spec and it seems reasonable. Making the listbox a child of the dialog itself and then assigning said listbox a [popover] attribute would work. I can imagine other issues, such as toasts while the dialog is open, but after more work combobox within dialog does seem doable.

Link2Twenty commented 2 months ago

There already is a concept of elements added later to the topLayer not becoming inert even if there is already a modal dialog open, opening a second dialog. I'd love for inert to only traverse backwards through the stack only rendering elements lower than the modal inert but I recognise that the web should strive to never change established behaviour even if we want it to.

Having someway of telling dialog that it's inert reach is only down the stack might be useful. Default behaviour stays as is but we have an new opt-in option.

There was talk a while ago of adding something to dialog to allow declaring it as a modal box in html. So this might be something that can be wrapped in with that.

<dialog type="default | modal | visual-modal">[...]</dialog>

Then we could do something like this.

<div popover>[...]</div> <!-- visually behind dialog, is inert -->
<dialog type="visual-modal">[...]</dialog>
<div popover>[...]</div> <!-- visually in front of dialog, is not inert -->

To take full advantage of this power we'd need a way to control stacking but that's a whole other issue.


In another issue I suggesting moving popover elements into open modal dialogs, in my example I used react createPortal but I'm fairly confident the same could be achieved with appendChild. I feel this is good enough for most use cases.

mangelozzi commented 2 months ago

i'm torn on this. because it'd be a really unexpected change in established behavior that a modal dialog can have content accessible outside of it. That essentially breaks the whole concept of a modal being modal (the content of the primary page being inert) - particularly in the case where content being revealed from a control in the modal dialog, but focus doesn't automatically move to it.... so why would anyone using a screen reader, for instance, even think that now there's content outside of the dialog that they can interact with?

For many related use cases that I've been made aware of, I would often end up suggesting that what "should" have been done is the additional popups/overs be descendants of the modal dialog, so a user (specifically user of AT) continues to remain in the context of the modal dialog - which is again, what has been the established expectation/promise of what a modal dialog represents - so the content that popups 'on top' of the dialog is actually accessible.

With that said, I acknowledge problems that exist where one might have global notifications that live outside of the modal dialog, and may be triggered due to an action within the dialog. These sorts of things do need to be exposed to users. The solution I've had to work with developers to implement is to essentially have modal dialogs have their own instance of notifications, which i understand is not ideal. Ideally the notification API proposal could help solve for some of these cases, but if a notification has a nested interactive element (e.g., a link) - well, that's just a whole separate issue that i'll not dive into here (yet?)

I just think it's really important to consider the ramifications of the ask - where there are definitely some use cases which would need this revised behavior. But I worry the simplification of "if a popover is displayed on top of a modal dialog, it should be interactable" glosses over the "well why is this UI built this way / does it need to be build this way?" and "why would someone invoke a modal dialog but also expect for people to interact with content that isn't part of that modal dialog, since the point of a modal is to make everything outside of it inert?" It seems to be coming from a place of "visually this makes sense" but it goes against what the developer has built with their markup. Again, i acknowledge there are some use cases that should be considered here, but the reduced test cases in this thread are too generic to draw any strong conclusions on as to whether the use situations that these reduced cases derived from are actually valid for consideration or not. The position of content in the DOM (and thus how it is exposed in the accessibility tree by browsers) is important to not make unpredictable.

From reading your post, I think what you are saying the main contract of a dialog is that everything else is inert when it's opened, am I understanding you correctly?

I say "when it's opened" because consider this case: When dialog A opens dialog B, is dialog A still interactable? ...as you know no it is not . So A made everything else inert at the time of it's opening, B also made everything else below inert. "Below it" being the whole DOM and everything else in the #toplayer below it.

Do you think a more accurate description of the dialog contract should then be is everything below it is inert. Which at the time of opening is everything. Only when the page designer allows other things to be opened in the top layer above it, are they buying into more interactable things. The description means if popup is opened in a dialog, then it should also be interactable, just like if a dialog is opened it becomes interactable too.

A nice UI pattern is notifications that stack up until they timeout/are dismissed. These are often required to be above a dialog. The suggestion of having two notifiers means you have two different stacks of notifications, which is a bit unusual to the user potentially seeing some notifications behind the modal backdrop, and some in front of the backdrop. To get around this we would have to just closer/reopen when a dialog is opened to force it to jump to the top.

I have been on the exact journey as the original posted, and the same frustration. I was so happy to finally see my notifier appear above the notifier... only to be followed by mass disappointment that I could not dismiss the notification. Once again a giant gotcha that makes the API unusable to something beyond a toy example.

mangelozzi commented 2 months ago

There already is a concept of elements added later to the topLayer not becoming inert even if there is already a modal dialog open, opening a second dialog. I'd love for inert to only traverse backwards through the stack only rendering elements lower than the modal inert but I recognise that the web should strive to never change established behavior even if we want it to.

I doubt there is a single example (if somebody knows of one please so share) of a single website that opens a dialog, that then opens a popover on top of the dialog to show it but have it inert. That sounds more like a bug (an element rendered on top the backdrop so it looks interactable, but is actually inert) than a feature to me. If that behavior is required one could just put an inert attribute on the popover.

What further leads me to believe the current behavior is a bug: Try document.elementFromPoint(x, y) on the x/y of a popover element that is above an open dialog, it returns the dialog element, not the popover element. This means from the browser's implementation point of view, the dialog is on top of the popover, but visually this is clearly not the case.

mangelozzi commented 2 months ago

As I understand it, allowing the popover element to be intractable sends us back to square one where we then need z-index within the top-layer itself. The entire purpose of the dialog element is defeated.

Having said that, one proposed solution can be as follows:

  • If the popover element is not a descendent of the dialog element - Show the popover but let it not be interactive. (So keep it as it is)
  • If however the popover element is a descendent of the dialog element - Make the popover interactive.

The second situation also ensures that the popover element necessarily opened after the dialog was opened. Which would be in line with the user's expectations that what was shown later is over on top of what was shown earlier. I believe this hits a compromise between the two ends and solves a few of the use-cases discussed above.

On a personal note: I strongly feel that having popover and dialog in the same layer is a no-win situation, we seem to be digressing in the earlier state in terms of layering elements on top of one another.

Unfortunately this solution does not solve the OP's original problem of a stacking notifier, since the notifier would not be a direct descendant of the dialog element that is currently open.

Neunerlei commented 2 weeks ago

I ran into the same issue this week while trying to get a long running project updated with a new modal which uses the dialog tag, but needs to support kendo-ui / selec2 / jquery-ui stuff for legacy reasons. A lot of overlays/dropdowns/filters are simply moved to the "body". I created a MutationObserver on the body to polyfill the "popover" feature for those elements, but while they show up above the modal, they are therefore not accessible in the modal.

What would really help in my opinion is to allow the elements to have their "inert" property set to FALSE in JS. That way, the specification would stay the same, but we could select explicit elements that use "popover" and are not inert.

lubomirblazekcz commented 1 week ago

This behaviour seems wrong. When a popover is on top of the layer of the dialog and it is visible, it should be also interactive. For example if you have dialog and a toaster for notifications, you would have to close the dialog to be able to interact with notifications, even if they are on top layer above dialog. That doesn't seem intuitive.

image

It's simple impossible doing stuff like Sooner / Toaster with native api's https://ui.shadcn.com/docs/components/sonner if you have an open dialog.