How Astryx's Layer System Handles z-Index and Stacking Contexts for Nested Popovers and Dialogs
Astryx eliminates manual z-index management by using the native HTML Popover API to promote every layer to a browser-managed top-layer stacking context, ensuring nested popovers and dialogs automatically stack in the correct order without explicit CSS z-index values.
Astryx is a modern React design system developed by Meta that prioritizes browser-native solutions over custom JavaScript implementations. The library's approach to z-index and stacking contexts follows this philosophy—rather than maintaining complex z-index registries or inline styles, Astryx delegates stacking order to the browser itself through the HTML Popover API. This article examines how useLayer and related components achieve reliable, nestable UI layers.
The Native Popover Strategy: No Explicit z-Index
According to the Astryx source code in packages/core/src/Layer/useLayer.tsx, the layer system deliberately avoids setting any z-index values. Instead, it relies on the popover attribute to trigger automatic top-layer promotion.
How the Popover Attribute Works
When you call useLayer(), the hook renders layers with attributes that tell the browser to treat them as native popovers:
// From useLayer.tsx – the popover attribute triggers top-layer promotion
<div popover={lightDismiss ? 'auto' : 'manual'}>
{/* Layer content here */}
</div>
The popover attribute accepts two values:
auto– Enables light-dismiss behavior (closes on outside click or Escape key) and allows the browser to manage focus and stackingmanual– Requires explicithidePopover()calls, but still promotes the element to the top-layer
In useLayer.tsx lines 70-73, Astryx conditionally sets this attribute based on the lightDismiss option. This single attribute accomplishes what traditionally required dozens of lines of z-index management code.
CSS Styles: Intentionally Minimal
The layer container styles in useLayer.tsx (lines 31-48) contain no z-index property:
const layerStyles = {
backgroundColor: 'transparent',
// No z-index here – stacking is handled by the browser's top-layer
};
This absence is deliberate. The top-layer is a separate rendering plane that sits above all regular document content, regardless of any z-index values in the page CSS. Elements in the top-layer stack according to creation order—newer elements appear above older ones.
Nested Popovers: Automatic Correct Ordering
Astryx's architecture makes nested popovers trivial to implement correctly. Because each layer renders as its own <div popover> element, the browser handles all ancestor-descendant relationships automatically.
Nested Popover Example
import { useLayer } from '@astryxdesign/core/Layer';
function NestedPopovers() {
const outer = useLayer({ mode: 'context', lightDismiss: true });
const inner = useLayer({ mode: 'context', lightDismiss: true });
return (
<>
<button ref={outer.ref}>Open Outer</button>
{outer.render(
<div style={{ padding: 16, background: '#f5f5f5', borderRadius: 8 }}>
<h3>Outer Popover</h3>
<p>This sits in the top-layer.</p>
<button ref={inner.ref}>Open Inner</button>
{inner.render(
<div style={{ padding: 16, background: 'white', borderRadius: 8, boxShadow: '0 8px 32px rgba(0,0,0,0.15)' }}>
<h4>Inner Popover</h4>
<p>This appears above the outer popover automatically.</p>
</div>,
{ placement: 'right', alignment: 'start' }
)}
</div>,
{ placement: 'below', alignment: 'center' }
)}
</>
);
}
When rendered:
- The outer popover enters the top-layer first
- The inner popover enters the top-layer second when triggered
- The browser's top-layer ordering guarantees the inner popover appears above without any z-index calculation
This behavior holds regardless of DOM nesting depth or CSS inheritance—each popover is an independent top-layer entity.
Context Mode: Anchor Positioning Without Stacking Side Effects
Astryx supports two positioning modes, both using the popover API for stacking:
| Mode | Positioning Mechanism | Stacking Behavior |
|---|---|---|
| Context | CSS Anchor Positioning (position-anchor, position-area, position-try-fallbacks) |
Native popover top-layer |
| Fixed | position: fixed with calculated coordinates |
Native popover top-layer |
In context mode, useLayer.tsx (lines 44-55) constructs dynamic anchor positioning styles:
// Anchor positioning for context mode
const anchorStyles = {
positionAnchor: `--${anchorName}`,
positionArea: `${blockAlign} ${inlineAlign}`,
positionTryFallbacks: 'flip-block, flip-inline',
};
These properties control where the popover appears relative to its trigger, but they do not affect how it stacks. The popover attribute remains the sole authority for stacking context creation.
The Exception: Registry-Based z-Index for Non-Popover Components
Astryx makes one concession to manual z-index management. For components that cannot use the native popover API—specifically non-modal drawers—the library falls back to a registry-based system.
As noted in packages/core/src/MobileNav/MobileNav.tsx (lines 16-18):
// Components not using the popover API require manual z-index assignment
// See: top-layer promotion comment for non-modal drawer implementation
This registry assigns incremental z-index values (typically starting at 1000) to ensure proper ordering when native top-layer promotion isn't available. However, this pattern is explicitly avoided for popovers, dialogs, tooltips, and hover cards—all standard layer components use the native API.
Provider Architecture: No Intermediary Stacking Logic
The LayerProvider component in packages/core/src/Layer/LayerProvider.tsx (lines 20-33) supplies application-level context for layers but notably does not intervene in z-index handling:
// LayerProvider.tsx – handles viewport management, not stacking
export function LayerProvider({ children }: LayerProviderProps) {
return (
<LayerContext.Provider value={contextValue}>
{children}
<div id="astryx-toast-viewport" /> {/* Toast container only */}
</LayerContext.Provider>
);
}
This separation of concerns reinforces Astryx's design: stacking is a browser responsibility, not a framework concern.
Summary
- No explicit z-index – Astryx layers contain zero z-index CSS properties; styling is limited to
backgroundColor: transparentand layout properties - Native top-layer promotion – The
popoverattribute (set inuseLayer.tsx) automatically creates a browser-managed stacking context above all regular content - Automatic nested ordering – Each layer receives its own
popoverelement; the browser orders multiple top-layer elements by creation time, guaranteeing nested popovers appear above ancestors - Positioning independence – Both context mode (CSS anchor positioning) and fixed mode use identical stacking strategies
- Limited manual fallback – Only non-popover components like certain drawers use registry-based z-index assignment
Frequently Asked Questions
What happens if two popovers open simultaneously—can they conflict?
No conflicts occur. The browser's top-layer specification mandates last-opened-wins ordering. When a second popover enters the top-layer through showPopover(), it implicitly stacks above any existing top-layer elements. Astryx does not need to track or coordinate this—the browser handles synchronization automatically.
Does Astryx's layer system work in browsers without popover API support?
The source analysis focuses on the canonical implementation targeting modern browsers. For legacy support, Astryx would likely fall back to the registry-based z-index pattern seen in the MobileNav component. However, the core useLayer implementation assumes popover API availability for its primary stacking mechanism.
Can developers override the stacking behavior with custom z-index values?
Attempting to add z-index to layer content has no effect on top-layer ordering. The popover attribute creates a separate stacking context that ignores document z-index values entirely. Developers wishing to control layer priority must manage open/close timing, as the browser orders top-layer elements chronologically.
How does focus management interact with the popover-based stacking system?
The popover="auto" setting (enabled via lightDismiss: true) grants the browser full control over focus trapping and restoration. When a layer opens, focus moves to the first focusable element; when closed via Escape or outside click, focus returns to the triggering element. This integration eliminates the need for custom focus management code in most cases.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →