Nested Overlays
Third-party overlays (menus, dialogs, popovers from other libraries) render in their own portals. When opened from inside an Axiom Dialog or Popover, they are not coordinated with Axiom’s modal layer, which causes two problems:
- Stacking — the third-party overlay renders behind the Axiom dialog because it does not share Axiom’s z-index band.
- Focus, scroll, and dismissal — the Axiom dialog traps focus, locks scroll, and owns the dismiss stack, so the nested overlay can be unusable.
The fix has two parts: wrap the portalled content in ModalLayer (to join Axiom’s focus/scroll/dismiss handling) and set its z-index to Axiom’s popover token (to lift it into Axiom’s stacking band). See Z-Index for the available tokens.
#
React Select
#
#
Rendering menus inside dialogs
#
Most functionalities of react-select work out of the box. The edge case is rendering its menu inside an Axiom Dialog.
-
Set
menuPortalTargettodocument.bodyto render the menu inside a portal.<Select menuPortalTarget={document.body} /> -
Wrap the
MenuPortalchildren inModalLayerso focus trapping is handled correctly inside the portal.import { ModalLayer } from "@optiaxiom/react"; import Select, { components } from "react-select"; const MenuPortal = ({ children, ...props }) => { return ( <components.MenuPortal {...props}> {props.appendTo ? <ModalLayer>{children}</ModalLayer> : children} </components.MenuPortal> ); }; <Select components={{ MenuPortal }} />; -
Set the
zIndexof theMenuPortalto ourpopovertoken so the menu is visible on top of the dialog.import { theme } from "@optiaxiom/react"; <Select styles={{ menuPortal: (styles) => ({ ...styles, zIndex: theme.zIndex.popover, }), }} />;
#
Material UI
#
#
Rendering dialogs inside dialogs
#
A Material UI Dialog opened from inside an Axiom Dialog renders behind it by default. Wrap it in ModalLayer and align its z-index with Axiom’s popover token.
-
Wrap the MUI dialog’s
PaperinModalLayervia thePaperComponentprop so it joins Axiom’s focus trapping. Define it as a stable module-level component, not inline.import { ModalLayer } from "@optiaxiom/react"; import { Dialog, Paper } from "@mui/material"; import { forwardRef } from "react"; const ModalLayerPaper = forwardRef((paperProps, ref) => ( <ModalLayer asChild> <Paper {...paperProps} ref={ref} /> </ModalLayer> )); function App() { return ( <Dialog PaperComponent={ModalLayerPaper}>{/* dialog content */}</Dialog> ); } -
Set MUI’s theme
zIndex.modalto thepopovertoken so the dialog stacks above Axiom’s overlays. Use the raw value fromtokenssince MUI expects anumber.import { tokens } from "@optiaxiom/react"; import { createTheme } from "@mui/material/styles"; const muiTheme = createTheme({ zIndex: { modal: Number(tokens.zIndex.popover), // 3000 — matches Axiom's popover band }, });
#
Overlays from outside React
#
#
When you choose the container
#
Some overlays are DOM appended by a script, such as an AngularJS overlay service. There’s no React subtree to wrap in ModalLayer, but you can give the script somewhere to append: render a container inside ModalLayer and have the script append its overlay there. The overlay then joins the dialog’s focus, scroll and pointer handling like any portalled React content.
import { ModalLayer } from "@optiaxiom/react";
import { createPortal } from "react-dom";
function OverlayContainer({ onContainer }) {
return createPortal(
<ModalLayer asChild>
<div ref={onContainer} />
</ModalLayer>,
document.body,
);
}Render it inside DialogContent when the overlay opens, then pass the node to the script. A ModalLayer that mounts in the same render as its DialogContent doesn’t join the dialog.
const [container, setContainer] = useState(null);
<DialogContent>
{overlayOpen && <OverlayContainer onContainer={setContainer} />}
</DialogContent>;#
When the script appends to body
#
Some scripts, like Google’s file picker, always append their own iframe to body. The dialog sets pointer-events: none outside itself, so the iframe shows but can’t be clicked. Set pointer-events: auto on the overlay’s root elements:
.picker-dialog,
.picker-dialog-bg {
pointer-events: auto;
}The class names belong to the third-party script, so check them in your browser’s devtools. If the overlay renders behind the dialog, also raise its z-index above the popover token.
This only works for iframes, because the dialog lets focus move into them. Plain DOM appended to body gets clicks this way but still loses focus and scrolling to the dialog: give it a container instead.