Skip to content

fix(portal): keep nested floating layers above inline containers - #219

Merged
ndlabdev merged 1 commit into
devfrom
fix/217-nested-floating-layers
Sep 18, 2026
Merged

ndlabdev merged 1 commit into
devfrom
fix/217-nested-floating-layers

Conversation

@ndlabdev

Copy link
Copy Markdown
Owner

Summary

Modal, Slideover, Drawer and Popover rendered with portal={false} hid every floating layer opened inside them whenever an ancestor set a z-index above 50. A Select inside a Slideover body opened, but its list stayed invisible behind the panel.

The nested layers were portalled to the end of <body> with z-50, while the inline panel belonged to the ancestor's higher stacking context, so the whole panel painted above them. The ScrollArea in the body was not involved: the list is position: fixed and is not clipped by overflow: hidden.

Closes #217

Type of change

  • 🐛 Bug fix
  • ✨ New feature / component
  • 📖 Documentation
  • ♻️ Refactor / chore
  • ⚠️ Breaking change

Changes

  • New internal PortalScope (not exported). While active, it renders an empty host element with fixed top-0 left-0 z-51 and points every nested portal without an explicit target at that host.
  • Modal, Slideover, Drawer, Popover: with portal={false}, the content is wrapped in PortalScope. The host sits right before the panel (before the overlay for a scrollable Modal), in the same stacking context and one level above it.
  • The host exists only while the container is open or still animating out. The panel element tracks that, so the closed DOM is identical to before and the host never becomes the first or last child of the parent. Earlier drafts that appended the host after the panel added space-y/divide-y margins and borders to the trigger and the panel.
  • Containers using the default portal are unchanged: no host is rendered and nested layers still portal to <body>.
  • 17 regression tests in the four specs, written without fixture files: no host while closed or portalled, host placed before the panel and never the last child, host z-index above the panel (and above the overlay of a scrollable Modal), host removed after closing. The placement, z-index and removal tests fail on dev; the no-host tests pass on both and guard against the host leaking into closed or portalled containers.
  • CHANGELOG.md entry under [Unreleased].

Checklist

  • Linked the related issue (Closes #…)
  • pnpm check passes (0 errors, 0 warnings)
  • pnpm lint passes
  • pnpm test passes
  • Added or updated tests for the change
  • Updated CHANGELOG.md under [Unreleased]
  • Followed component conventions (no comments outside *.types.ts, Material 3 design tokens)

Screenshots / notes

Verified in Chromium against dev, with the container inside relative z-[60]:

  • Select, SelectMenu, DatePicker, DateRangePicker, DropdownMenu, Popover, Tooltip, ContextMenu, a nested Modal and Lightbox are fully visible inside all four containers and a scrollable Modal (fully or partly hidden on dev).
  • Positions relative to the trigger are unchanged, and layers keep following the trigger while the overlay, the ScrollArea, the drawer body or the page scrolls.
  • Escape, focus return, outside click and nested modals behave as on dev. Layout of the parent (space-y, divide-y, flex gap, first/middle/last position) is identical when closed and open.
  • Also checked: a container and a Select both open on first render (host is present in the SSR HTML, no hydration warnings), and a Popover with portal={false} nested in a Slideover with portal={false}.

Known limits:

  • A container whose z-index is raised above 51 through ui.content still hides nested layers, as before.
  • While a container is open, :nth-child selectors in its parent count one extra element.
  • Only verified in Chromium.

Out of scope: Tour, NavigationMenu and Toaster can show the same symptom through their own z-index, but are left unchanged.

Modal, Slideover, Drawer and Popover rendered with portal={false} hid
every floating layer opened inside them (Select, DatePicker,
DropdownMenu, Tooltip, a nested Modal, ...) whenever an ancestor set a
z-index above 50. The layers were portalled to <body> at z-50 while the
inline panel belonged to the higher stacking context.

While such a container is open, nested layers now mount into a host
placed right before its panel, in the same stacking context and one
level above it. The host is removed once the panel has unmounted, so the
closed DOM is unchanged, and portalled containers are not affected.

Closes #217
@ndlabdev ndlabdev added the bug Something isn't working label Sep 17, 2026
@ndlabdev ndlabdev self-assigned this Sep 17, 2026
@ndlabdev
ndlabdev merged commit 8788663 into dev Sep 18, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant