Repository navigation
Conversation
Toasts and dropdowns rendered behind an open modal, and once they
were lifted above it their buttons still could not be clicked -
clicking one closed the modal instead of activating the button.
Both symptoms trace to one line in modal.tsx:
if (!dialog.open) dialog.showModal()
showModal() does two things. It moves the dialog into the browser's
top layer, which no z-index can ever beat, and it marks everything
outside the dialog as inert. Toasts and dropdowns were portaled to
document.body, so they lost on both counts: painted underneath, and
inert. An inert element takes no pointer events, so the click fell
through to the <dialog> - which daisyUI's .modal stretches over the
whole viewport - and matched the backdrop-close check
(e.target === dialogRef.current), closing the modal.
Portal now takes a topLayer flag. Such a portal renders into a host
element it owns, promotes it with the Popover API so it paints above
an open dialog, and appends it to the topmost open modal dialog when
there is one, since that subtree is the only region that is not
inert. A one-shot close listener moves the host back to the body
when that dialog closes, so a toast already on screen does not
vanish with the modal.
The host element is created imperatively rather than rendered as
JSX: React would otherwise own it as a portal root and throw
NotFoundError on unmount after we re-parented it away from
document.body.
The host spans the viewport, so it defaults to pointer-events: none
and children opt back in; without that it would swallow every click
on the page. The dropdown already sets pointer-events-auto, and
react-hot-toast marks each toast wrapper "> * { pointer-events:
auto }".
The toaster mounts once at startup, before any modal exists, so it
would sit below a later dialog in top layer order. Every toast entry
point re-raises the layer first.
The bookmark three-dot menu rendered behind the bookmark folder modal, and both tooltip components had the same defect. All three called createPortal(..., document.body) directly and relied on a z-index, which cannot win against a dialog opened with showModal(); they were also inert while a modal was open, so their buttons took no clicks. Moved them onto Portal's topLayer flag and marked their content pointer-events-auto, since the top layer host is transparent to pointer events. The tooltips are reached from inside the settings modal (wallpapers and account tabs), so they hit this in practice rather than in theory. canvas-edit-toolbar.tsx still portals straight to document.body and is deliberately left alone: it belongs behind a modal, since opening "add widget" from it should cover it.
Opening the language picker in voice search showed a menu cut off at the panel's edge. Unlike the other overlay bugs fixed alongside it, this one has nothing to do with modals or the top layer. The menu was a hand rolled popover: <div className="absolute bottom-full mb-2 left-0 ... min-w-[120px]"> and the voice search panel it sits in is overflow-hidden, so the menu was clipped as soon as it extended past the panel. Replaced it with the Dropdown primitive, which portals its content out and cannot be clipped by an ancestor. Per AGENTS.md, popovers are not hand rolled here. The local open/close state goes away with it, since Dropdown owns that.
In the shop, moving the pointer anywhere inside the modal revealed the "پیشنمایش" button on every wallpaper at the same time, without the pointer being over any of them. The buttons are plain CSS hover, not tooltips: opacity-0 group-hover:opacity-100 and each card does carry its own group. The problem is what group-hover compiles to: .group-hover\:opacity-100:is(:where(.group):hover *) That matches any hovered ancestor carrying .group, not the nearest one, and modalDialogVariants puts group on the <dialog> itself so modal-box can drive its open animation from group-open. The dialog is an ancestor of everything in the modal and covers the whole viewport, so as soon as the pointer entered it every group-hover inside fired together. Switched the dialog to a named group. It is now group/modal and the four open-state utilities are group-open/modal, so the dialog's marker is no longer .group and descendant group-hover cannot see it. Naming the modal's group rather than renaming the cards' is what fixes the whole class of this: group-open appears four times, all in this file, while group-hover appears across 39 files, so the leak affected every modal in the app rather than only the wallpapers. The open and close animation is unchanged - all four rules, both @starting-style blocks included, were checked in the built CSS. This predates the overlay work on this branch; group has been on the dialog all along.
…nd-modal-top-layer
…y-app/widgetify-extension into fix/overlay-stacking-behind-modal-top-layer
sajjadmrx
approved these changes
Sep 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Overlays rendered behind an open modal, and once lifted above it they still could not be clicked — clicking a toast button closed the modal instead of pressing the button. Reported for toasts, the habit widget dropdown, the bookmark three-dot menu, and the voice search language picker.
Root cause
showModal()does two separate things, and both bit us:z-indexcan beat that.e.target === dialogRef.current.Changes
Portalgains atopLayerflag (existing component, no new files). It renders into a host it owns, promotes it via the Popover API so it paints above an open dialog, and appends it to the topmost open modal dialog — the only non-inert region. A one-shotcloselistener moves the host back todocument.bodyso a toast already on screen survives the modal closing.NotFoundErroron unmount once we re-parented it.pointer-events: none(it spans the viewport, so otherwise it would swallow every click); children opt back in.react-hot-toastalready marks each toast wrapper> * { pointer-events: auto }.Dropdown, bookmark context menu,Tooltip,ClickableTooltip.absolutepopover clipped by the panel'soverflow-hidden. Replaced with theDropdownprimitive, per AGENTS.md.Also fixed here: every hover overlay in a modal revealing at once
In the shop, moving the pointer anywhere inside the modal revealed the "پیشنمایش" button on every wallpaper simultaneously.
Those buttons are plain CSS hover (
opacity-0 group-hover:opacity-100), and each card does carry its owngroup. Butgroup-hovercompiles to:which matches any hovered ancestor carrying
.group, not the nearest one — andmodalDialogVariantsputsgroupon the<dialog>itself somodal-boxcan drive its open animation fromgroup-open. The dialog wraps everything and covers the viewport, so entering it fired everygroup-hoverinside.The dialog now uses a named group (
group/modal, with the four utilities asgroup-open/modal), so its marker is no longer.group. Naming the modal's group rather than the cards' is what fixes the whole class:group-openappears 4 times in one file,group-hoveracross 39 files — the leak affected every modal, not just wallpapers.This one predates the branch;
grouphas been on the dialog all along.Deliberately left out
canvas-edit-toolbar.tsxstill portals straight todocument.body. It belongs behind a modal — opening "add widget" from it should cover it.advanced.modal.tsxis also a hand-rolledabsolutepopover prone to the same clipping, but it was not reported and is not inert, so it is left as-is.Testing
npm run compilenpm testnpx biome check srcnpm run buildgroup-open/modalrules, both@starting-styleblocks included