Shadow DOM Integration
How to render @acronis-platform/ui-react inside a shadow-DOM micro-frontend with fully styled portaled components and zero host style leakage.
When @acronis-platform/ui-react runs inside a shadow-DOM micro-frontend
(MFE), portaled components — Popover, DropdownMenu, Tooltip, Dialog, Sheet,
Select, Combobox, and Toaster — mount their popups into document.body by
default. This means the popup escapes the shadow boundary and loses access to
the adopted stylesheets, rendering unstyled.
PortalContainerProvider solves this by redirecting all portals into a container
inside the shadow root.
Quick setup
If your host already renders the app inside its shadow root, all you need is a container element to point the provider at:
import { useState } from 'react';
import { PortalContainerProvider } from '@acronis-platform/ui-react';
function ShadowMFE() {
// Hold the mount element in state, not a ref: the provider reads `container`
// during render, and populating a ref doesn't trigger one. With a ref, the
// provider keeps the `null` it saw on the first render — and a `null`
// container renders no popup at all, so overlays silently never open.
const [mount, setMount] = useState<HTMLElement | null>(null);
return (
<div ref={setMount}>
<PortalContainerProvider container={mount}>
<App />
</PortalContainerProvider>
</div>
);
}That's it. Every portaling component automatically picks up the container from
context. No need to pass portalContainer to each component individually.
Passing the state setter straight to ref is the shortest correct form — React
calls it with the element on mount (and with null on unmount), so the provider
re-renders exactly once with the real container.
If your app also creates the shadow root, attach it and render into it with
createPortal first — see
ShadowDemo.tsx,
which is what powers the live previews on these component pages — then adopt the
stylesheet as shown in Adopting styles.
How it works
PortalContainerProvidersets a React context with the target DOM element.- Each portaling component calls
usePortalContainer()internally to read the context. - The component passes the resolved container to the Base UI
Portalprimitive, which creates a React portal into that element instead ofdocument.body. - An explicit
portalContainerprop on any individual component always takes precedence over the context — useful for edge cases where one popup needs a different container.
Affected components
All portaling components respect PortalContainerProvider:
| Component | Portal target |
|---|---|
PopoverContent | Positioner + popup |
DropdownMenuContent | Positioner + popup |
TooltipContent | Positioner + popup |
Dialog | Backdrop + popup |
SheetContent | Backdrop + popup |
SelectContent / InputSelectContent | Positioner + popup |
ComboboxContent | Positioner + popup |
Toaster | Viewport + toasts |
Adopting styles
ui-react's CSS is authored for :root, :host, so the same stylesheet works
inside a shadow root. The recommended approach:
// Fetch or import ui-react's compiled CSS
import cssText from '@acronis-platform/ui-react/styles?raw';
// Build a Constructable Stylesheet and adopt it
const sheet = new CSSStyleSheet();
sheet.replaceSync(cssText);
shadowRoot.adoptedStyleSheets = [sheet];This keeps ui-react's Tailwind Preflight and design tokens inside the shadow boundary — they never touch the host document.
Theming inside the shadow root
ui-react uses light-dark() with the [data-theme] attribute. Set it on your
mount element:
<div data-theme="light">
<PortalContainerProvider container={mountEl}>
<App />
</PortalContainerProvider>
</div>To follow the host page's theme, observe document.documentElement for
data-theme or class changes and mirror them onto your shadow mount.
Overriding per-component
If a specific component needs to portal elsewhere (e.g. a full-screen dialog
that must overlay the entire viewport), pass portalContainer directly:
<PortalContainerProvider container={shadowMount}>
{/* This popover portals into the shadow root (from context) */}
<Popover>
<PopoverTrigger>Open</PopoverTrigger>
<PopoverContent>Inside shadow</PopoverContent>
</Popover>
{/* This dialog portals to document.body (explicit override) */}
<Dialog portalContainer={document.body} open>
Full-screen overlay
</Dialog>
</PortalContainerProvider>Known caveats
- Positioning: Base UI's own default positioner is
position: absolute, which shares its portal container's containing block — inside aoverflow: hiddenshadow-root host, that clips the popup at the host's own edge.PopoverContentaccounts for this: when portaled into a resolvedportalContainer, it defaultspositionMethodto'fixed'instead, which escapes a plain overflow-clipping ancestor.'fixed'still breaks if an ancestor hastransform,contain,will-change,filter, orbackdrop-filterset — those create a new containing block for fixed positioning too. Avoid these CSS properties on shadow-root ancestors. The other portaling components (Tooltip, Toast, DropdownMenu, InputSelect, Combobox, Dialog, Sheet) don't yet defaultpositionMethodthis way, so the example above — aDialoginside aPortalContainerProvider— can still clip at the shadow host's edge. - z-index: Portaled content inside a shadow root participates in the shadow root's stacking context, not the document's. This is usually desirable (popups stay inside the MFE), but means a shadow-hosted popup cannot overlay content outside the shadow root.
- Focus management: Base UI's focus trapping and restoration work across shadow boundaries in modern browsers (Chrome 90+, Firefox 96+, Safari 16.4+).
API reference
Prop
Type