# How the Astryx Layer System Manages Z-Index and Stacking Contexts

> Discover how Astryx manages z-index and stacking contexts using CSS @layer, anchor positioning, and the top-layer API, eliminating manual CSS.

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

---

**Astryx eliminates manual z-index management by using CSS `@layer` ordering, CSS anchor positioning, and the browser's native top-layer API, with numeric z-index values assigned only to non-modal drawers via an internal registry.**

The **Astryx Layer system** in the `facebook/astryx` repository redefines how UI components stack and overlap by replacing traditional z-index gymnastics with modern CSS layering techniques. Instead of arbitrary numeric values, the system leverages build-time CSS layers and runtime anchor positioning to create deterministic stacking contexts. This approach prevents the common "z-index wars" that plague complex design systems while maintaining strict isolation between library and consumer styles.

## CSS Layer Architecture at Build Time

Astryx establishes its stacking foundation during the build process through a Vite plugin that injects a strict layer hierarchy. In [`packages/build/src/vite.ts`](https://github.com/facebook/astryx/blob/main/packages/build/src/vite.ts) (lines 71-78), the plugin inserts a CSS `@layer` rule that defines the order:

```css
@layer reset, astryx-base, astryx-theme, product;

```

This declaration guarantees that **reset** styles apply first, followed by **astryx-base** (core library styles), then **astryx-theme** (theme customizations), and finally **product** (consumer application styles). By enforcing this cascade, Astryx ensures library styles always sit below application styles without specificity battles.

The same plugin (lines 62-87) prefixes **StyleX** atoms to prevent collisions across these layers. Library atoms receive the `astryx` prefix, while product atoms use a distinct configurable prefix, creating physical separation between framework and application CSS even before the browser renders the page.

## Native Top-Layer and Anchor Positioning

The `useLayer` hook—implemented in [`packages/core/src/Layer/useLayer.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/Layer/useLayer.tsx)—eliminates z-index entirely for popovers, tooltips, and dropdowns. Rather than setting numeric stacking values, the hook relies on **CSS anchor positioning** and the browser's native **top-layer** promotion.

When invoked, `useLayer` injects styles for `position-area` and `position-try-fallbacks` (lines 31-53 and 60-66) to position elements relative to their anchors. Because these elements utilize the Popover API, the browser automatically promotes them to the top-layer above all regular document content, making manual z-index values redundant.

## Fallback Behavior for Legacy Browsers

When the Popover API is unavailable, `useLayer` degrades gracefully without introducing z-index complexity. In [`packages/core/src/Layer/useLayer.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/Layer/useLayer.tsx) (lines 94-100), the hook simply toggles element visibility while keeping the component in normal document flow. This maintains accessibility and functionality without the "z-index creep" that typically accompanies polyfill implementations.

## Explicit Stacking for Non-Modal Drawers

Astryx assigns numeric z-index values in only one specific scenario: **non-modal drawers** that cannot utilize the top-layer API. In [`packages/lab/src/Drawer/Drawer.tsx`](https://github.com/facebook/astryx/blob/main/packages/lab/src/Drawer/Drawer.tsx) (lines 69-79), the component registers itself with an internal **z-index registry** that assigns incremental numeric values to each open drawer instance.

This registry ensures that later drawers stack above earlier ones, maintaining logical order when multiple non-modal drawers are visible simultaneously. Modal drawers, by contrast, use the native `<dialog>` element's top-layer behavior and require no manual stacking management.

## Toast and Viewport Management

Toast notifications stack above other UI elements without explicit z-index through the `LayerProvider` system. In [`packages/core/src/Layer/LayerProvider.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/Layer/LayerProvider.tsx) (lines 51-60), the provider renders a **ToastViewport** as a fixed-position container placed after the CSS `@layer` declarations in the DOM.

Because this viewport exists outside the normal stacking context of layered components and relies on DOM order within the top-layer, toast messages naturally overlay all other interface elements. The system handles multiple toasts through a configuration object that manages visibility limits rather than z-index values.

## Isolation Patterns in Navigation Components

Many Astryx components intentionally avoid declaring any z-index to prevent "leaky" stacking contexts. Components like `MobileNav` and `TopNavMegaMenu` rely entirely on the browser's top-layer or CSS `@layer` isolation, ensuring that navigation overlays never accidentally slide beneath content due to competing z-index values in parent containers.

## Practical Implementation Examples

### Basic Popover Using `useLayer`

This example demonstrates the standard pattern for creating anchored overlays without z-index management:

```tsx
import {useLayer} from '@astryxdesign/core';

function Example() {
  const layer = useLayer({mode: 'context'});
  return (
    <>
      <button ref={layer.ref}>Hover me</button>
      {layer.render(
        <div className="my-popover">Popover content</div>,
        {placement: 'below', alignment: 'center'}
      )}
    </>
  );
}

```

The hook automatically injects `position-area` styles and manages the native popover state, causing the element to appear above all page content without explicit stacking values.

### Non-Modal Drawer with Registry Assignment

When using non-modal drawers, the component automatically receives its z-index from the internal registry:

```tsx
import {Drawer} from '@astryxdesign/lab';

function NonModalDrawer() {
  return (
    <Drawer
      open
      onClose={() => {}}
    >
      Drawer content
    </Drawer>
  );
}

```

The `Drawer` component registers itself on mount and receives a higher numeric z-index than any previously opened drawer, ensuring proper stacking order without developer intervention.

### Global Toast Configuration with LayerProvider

To implement system-wide toast notifications without z-index conflicts:

```tsx
import {LayerProvider, useToast} from '@astryxdesign/core';

function App() {
  const toast = useToast({position: 'topEnd', maxVisible: 3});
  return (
    <LayerProvider toast={toast.config}>
      <MainUI />
      <button onClick={() => toast.show('Saved!')}>Show toast</button>
    </LayerProvider>
  );
}

```

The `LayerProvider` wraps the application and injects the `ToastViewport` after the CSS layer declarations, ensuring toast messages render above all other content through DOM ordering rather than explicit stacking values.

## Summary

- **Astryx** manages stacking primarily through **CSS `@layer` ordering** defined at build time in [`packages/build/src/vite.ts`](https://github.com/facebook/astryx/blob/main/packages/build/src/vite.ts), establishing a strict hierarchy of reset, base, theme, and product layers.
- The **`useLayer` hook** leverages **CSS anchor positioning** and the browser's native **top-layer** API to position popovers without any z-index values, as implemented in [`packages/core/src/Layer/useLayer.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/Layer/useLayer.tsx).
- **Non-modal drawers** represent the sole exception where Astryx assigns numeric z-index values, using an internal registry in [`packages/lab/src/Drawer/Drawer.tsx`](https://github.com/facebook/astryx/blob/main/packages/lab/src/Drawer/Drawer.tsx) to maintain proper stacking order among multiple concurrent drawers.
- **Toast notifications** and other layered elements rely on DOM placement within the `LayerProvider` and fixed positioning to achieve overlay behavior without manual z-index management.
- Components like `MobileNav` and `TopNavMegaMenu` intentionally omit z-index declarations to prevent stacking context leakage and reliance on the browser's inherent layer isolation.

## Frequently Asked Questions

### Does Astryx use z-index for popovers and tooltips?

No. According to the source code in [`packages/core/src/Layer/useLayer.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/Layer/useLayer.tsx), the `useLayer` hook never sets a z-index property. Instead, it positions elements using CSS anchor positioning (`position-area`, `position-try-fallbacks`) and relies on the browser's native top-layer API to ensure popovers appear above regular page content.

### How does Astryx handle browsers without native Popover API support?

When the Popover API is unavailable, the `useLayer` hook (lines 94-100 in [`packages/core/src/Layer/useLayer.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/Layer/useLayer.tsx)) falls back to toggling element visibility while keeping the component in normal document flow. This approach avoids introducing z-index complexity for legacy browser support while maintaining functional accessibility.

### What is the CSS layer order in Astryx?

The layer order is defined in [`packages/build/src/vite.ts`](https://github.com/facebook/astryx/blob/main/packages/build/src/vite.ts) (lines 71-78) as: `@layer reset, astryx-base, astryx-theme, product;`. This hierarchy ensures that reset styles apply first, followed by Astryx core styles, theme customizations, and finally product application styles, creating predictable cascade behavior without specificity conflicts.

### When does Astryx assign numeric z-index values?

Astryx assigns numeric z-index values only to **non-modal drawers**, as implemented in [`packages/lab/src/Drawer/Drawer.tsx`](https://github.com/facebook/astryx/blob/main/packages/lab/src/Drawer/Drawer.tsx) (lines 69-79). These components cannot use the browser's top-layer API because they must remain interactive with the rest of the page, so they receive incremental z-index values from an internal registry to maintain proper stacking order when multiple drawers are open simultaneously.