How the Astryx Layer System Manages Z-Index and Stacking Contexts
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 (lines 71-78), the plugin inserts a CSS @layer rule that defines the order:
@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—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 (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 (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 (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:
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:
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:
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
@layerordering defined at build time inpackages/build/src/vite.ts, establishing a strict hierarchy of reset, base, theme, and product layers. - The
useLayerhook leverages CSS anchor positioning and the browser's native top-layer API to position popovers without any z-index values, as implemented inpackages/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.tsxto maintain proper stacking order among multiple concurrent drawers. - Toast notifications and other layered elements rely on DOM placement within the
LayerProviderand fixed positioning to achieve overlay behavior without manual z-index management. - Components like
MobileNavandTopNavMegaMenuintentionally 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, 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) 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 (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 (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.
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 →