Keeping Injected UI Above the Page with the Top Layer

Stop page headers, banners and stacking contexts from covering extension UI: why z-index fails, the top layer via popover and dialog, transform and overflow traps, and coexisting with the page's own modals.

Published October 2, 2026 Updated October 2, 2026 8 min read
Table of Contents

Your injected panel has z-index: 2147483647 — the largest value CSS allows — and a cookie banner still covers it on one site, a sticky video player on another, and a site’s own sign-in modal on a third. Developers respond by fighting fire with fire: !important, re-appending the element to the end of body every second, MutationObservers that push the z-index up. None of it works reliably, because z-index is not a global ranking. The modern answer is to step outside the stacking system entirely with the top layer. This guide explains the mechanics and the remaining edge cases. It belongs to in-page overlays and injected UI.

Why the biggest z-index can still lose

Z-index only compares elements within the same stacking context. Many CSS properties create a new stacking context — transform, filter, opacity below 1, will-change, isolation: isolate, position: fixed with a z-index, contain: paint and others. An element inside a stacking context can never rise above elements outside it, whatever its own z-index. If your UI’s host is appended inside a container that creates a context (a common pattern for SPA root elements), your maximum z-index is trapped inside that container. And even a host at the document root ties with a page element that also uses the maximum, at which point document order decides. The top layer is a separate rendering layer above the whole document, used by modal dialogs, popovers and fullscreen elements; anything in it renders above every stacking context, regardless of z-index.

Rendering order from bottom to topThe root stacking context with its children, nested stacking contexts created by transforms and filters that trap their descendants, the highest z-index element in the root context, and finally the top layer used by dialogs and popovers.Top layerdialog.showModal(), :popover-openlast opened on topRoot context, z-index maxyour fixed host at <html>ties go to DOM orderNested context (transform)SPA root, animated wrappertraps descendantsNormal flowpage contentz-index auto
No z-index escapes its stacking context; the top layer sits above all of them.

Step-by-step: reliably on top

1. Mount the host at the document root

1const host = document.createElement("readable-ui");
2host.style.cssText = "all: initial; position: fixed; inset: 0 0 auto auto; z-index: 2147483647;";
3document.documentElement.append(host);          // not inside body, never inside page containers

Execution context: the content script. Appending to <html> places the host in the root stacking context, as late in document order as possible, which wins ties against page elements with the same z-index. This is enough for persistent small controls such as a floating button — it beats almost all page chrome — without the behavioural changes the top layer brings.

2. Put overlays that must win into the top layer

1const panel = document.createElement("div");
2panel.setAttribute("popover", "manual");
3panel.className = "panel";
4shadowRoot.append(panel);
5panel.showPopover();                            // now in the top layer

Execution context: the content script, with the panel inside your closed shadow root. Popovers are promoted to the top layer when shown, even from inside a shadow root nested in an element with transform — the stacking context of the host no longer matters. manual popovers stay open until you hide them; auto popovers close on outside click and Escape, and closing one auto popover can close others, so use manual for persistent panels. Feature-detect with "showPopover" in HTMLElement.prototype.

Which container for this piece of UI?Decision tree: small persistent controls use a fixed host with maximum z-index; transient panels and menus use manual or auto popovers in the top layer; blocking decisions use modal dialogs.What kind of UI is it?small, persistentFixed hostz-index max at <html>Page modals cover itwhich is correctpanel or menuPopovertop layermanual or autoby dismissal needsblocking decisionModal dialogshowModal()Page inertuntil closed
Use the lightest mechanism that reliably stays visible.

3. Avoid trapping your own UI

1/* inside your shadow root — do not create contexts on the host's ancestors you control */
2:host { all: initial; }          /* no transform, filter or contain on the host */
3.panel { position: fixed; }      /* popovers default to fixed in the top layer anyway */

Execution context: the shadow root’s stylesheet. Animating the host with transform or applying filter: drop-shadow() to it creates a stacking context around everything inside — harmless for top-layer popovers, fatal for anything that relies on z-index. Animate inner elements instead. Likewise, overflow: hidden on the host clips descendants that are not in the top layer.

4. Coexist with the page’s own top-layer UI

1// Before opening your panel, check whether the page has a modal open
2const pageModalOpen = document.querySelector("dialog[open]")?.matches(":modal");
3if (pageModalOpen) {
4  queueUntilPageModalCloses(() => panel.showPopover());
5} else {
6  panel.showPopover();
7}

Execution context: the content script. The top layer is shared: if the page opens a modal dialog after your popover, theirs is on top and yours becomes inert behind it, which is correct — the page’s modal is asking the user something. If yours opens after theirs, yours is on top but the page’s modal still makes the rest of the page inert, and your popover’s interactive elements may be inert too unless it is inside the active modal. Waiting for the page’s modal to close avoids the conflict. A MutationObserver on dialog[open] attributes detects when it does.

Two owners of the top layerThe page opens its sign-in modal; the extension wants to show a panel, detects the page modal, waits; the page modal closes; the extension shows its popover in the top layer.PageTop layerContent scriptdialog.showModal() (sign-in)wants to open p…page modal open? → waitdialog.close()panel.showPopover()
Being polite in the top layer avoids fighting the page for focus.

5. Handle fullscreen elements

1document.addEventListener("fullscreenchange", () => {
2  const fs = document.fullscreenElement;
3  if (fs && panel.matches(":popover-open")) {
4    // Popovers opened before fullscreen render beneath the fullscreen element in some engines
5    panel.hidePopover();
6    panel.showPopover();                       // re-open to move above it
7  }
8}, { signal });

Execution context: the content script. A fullscreen video is itself in the top layer. Whether your popover appears over it depends on order: last added is on top. Re-showing after fullscreenchange moves it to the top — though for most features the right behaviour during fullscreen video is to stay hidden and not interrupt.

6. Fall back where the top layer is unavailable

1const hasPopover = "showPopover" in HTMLElement.prototype;
2if (!hasPopover) {
3  panel.style.cssText += "position: fixed; z-index: 2147483647;";
4  panel.hidden = false;                       // best effort: root context, maximum z-index
5}

Execution context: the content script on older engines. Without the Popover API, a fixed element in a root-context host with the maximum z-index is the best available. A modal <dialog> is the other route into the top layer and has broader support than popovers, but it makes the page inert, which is wrong for a non-blocking panel.

Stacking techniques and what defeats themMaximum z-index in a nested container, maximum z-index at the document root, popover, and modal dialog, with the page conditions that still cover each.TechniqueBeaten bySide effectsMax z-index, nestedAny ancestor contextNoneMax z-index at <html>Page top layer, tiesNonePopover (manual)Later top-layer itemsNoneModal dialogLater top-layer itemsPage inert
Only the top layer is immune to the page's stacking contexts.

Common mistakes

  • Escalation loops. Observers that keep raising z-index or re-appending the host fight the page’s scripts, waste CPU, and still lose to stacking contexts.
  • Mounting inside body > #app. SPA roots often have transform or isolation, trapping your UI.
  • Using auto popovers for persistent panels. They close on any outside click and can close each other.
  • Covering the page’s own modals. If the site is asking for a password or consent, your panel should wait.
  • Animating the host with transforms. It creates a stacking context and changes how fixed-position descendants behave.

Cross-browser variation

  • Chrome / Edge: Popover API from 114; top layer for dialogs, popovers and fullscreen.
  • Firefox: Popover API from 125; earlier versions need the root-context fallback.
  • Safari: Popover API from 17. On iOS, fixed-position elements outside the top layer can be covered by the browser’s own toolbars while scrolling; popovers are placed more predictably.

Verification

  1. On a page with a sticky header using z-index: 2147483647, open your panel: it appears above the header.
  2. Wrap the page’s root in transform: translateZ(0) via DevTools and confirm the panel is still on top.
  3. Open the page’s own modal, then trigger your panel: it waits, and appears when the modal closes.
  4. In DevTools’ Elements panel, confirm the panel shows a #top-layer badge while open.

FAQ

Is the top layer allowed for extension UI?

Yes. Popovers and dialogs are standard web features available to content scripts like any other DOM API.

Can the page remove my popover from the top layer?

It can remove your host from the DOM, which removes the popover. It cannot call methods on elements inside a closed shadow root.

Does the top layer work inside iframes?

Each document has its own top layer. A popover in an iframe is on top within that iframe’s viewport, not over the parent page.

Other UI/UX Patterns & Interactive Components Resources