# How Astryx's Layer System Handles z-Index and Stacking Contexts for Nested Popovers and Dialogs

> Astryx's Layer system simplifies z-index management. Discover how it leverages the HTML Popover API for automatic stacking of nested popovers and dialogs without CSS.

- Repository: [Meta/astryx](https://github.com/facebook/astryx)
- Tags: deep-dive
- Published: 2026-08-04

---

**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`](https://github.com/facebook/astryx/blob/main/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:

```tsx
// 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 stacking
- **`manual`** – Requires explicit `hidePopover()` calls, but still promotes the element to the top-layer

In [`useLayer.tsx`](https://github.com/facebook/astryx/blob/main/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`](https://github.com/facebook/astryx/blob/main/useLayer.tsx) (lines 31-48) contain **no z-index property**:

```tsx
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

```tsx
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:

1. The **outer popover** enters the top-layer first
2. The **inner popover** enters the top-layer second when triggered
3. 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`](https://github.com/facebook/astryx/blob/main/useLayer.tsx) (lines 44-55) constructs dynamic anchor positioning styles:

```tsx
// 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`](https://github.com/facebook/astryx/blob/main/packages/core/src/MobileNav/MobileNav.tsx) (lines 16-18):

```tsx
// 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`](https://github.com/facebook/astryx/blob/main/packages/core/src/Layer/LayerProvider.tsx) (lines 20-33) supplies application-level context for layers but notably **does not intervene in z-index handling**:

```tsx
// 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: transparent` and layout properties
- **Native top-layer promotion** – The `popover` attribute (set in [`useLayer.tsx`](https://github.com/facebook/astryx/blob/main/useLayer.tsx)) automatically creates a browser-managed stacking context above all regular content
- **Automatic nested ordering** – Each layer receives its own `popover` element; 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.