Floating portals

Updated: Sep 24, 2026
Render floating surfaces through the shared portal root. Keeping overlays out of their anchor’s inline DOM prevents ancestor overflow and stacking contexts from clipping them and keeps them above page content.
Everything the toolkit floats resolves the same root:
  • Tooltips raised by an interactable’s tooltipText or tooltipContent, which means every control built on InteractableBase.
  • The standalone Tooltip surface.
  • ContextMenu and VerticalMenu, which are anchored as focusable tooltip content on their trigger.
  • Toasts queued with useToast and rendered by ToastContainer.
  • The expanded text surface Modal opens when its subtitle is truncated.
App creates that root for you: a full-size, pointer-transparent layer that is the last child of its content root, published through FloatingPortalRootProvider. Ordinary applications should let App own this API.

Usage

Use the provider only when you embed the toolkit inside a host that must own the overlay layer itself — for example a canvas that positions the app inside a larger document, where overlays have to be clipped to that region.
Scope overlays to a subtree by rendering a provider around it and pointing the provider at an element inside that subtree. Hold the element in state rather than a ref so the subtree re-renders once the element exists.
import {useState, type ReactNode} from 'react';
import {FloatingPortalRootProvider} from '@wearables-ui-toolkit/mrbd';
import styles from './EmbeddedSurface.module.css';

export function EmbeddedSurface({children}: {children: ReactNode}) {
  const [portalRoot, setPortalRoot] = useState<HTMLDivElement | null>(null);

  return (
    <div className={styles.surface}>
      <FloatingPortalRootProvider root={portalRoot}>
        {children}
      </FloatingPortalRootProvider>
      <div className={styles.overlayLayer} ref={setPortalRoot} />
    </div>
  );
}
.surface {
  position: relative;
  overflow: hidden;
  border-radius: var(--uit-corner-radius-medium);
  background: var(--uit-color-background-window);
}

.overlayLayer {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
Keep the overlay element the last child of the region it covers so it paints over the content, give it pointer-events: none so it never intercepts input meant for the content below, and make the region a positioned ancestor so absolute coordinates resolve against it.
Content outside the provider keeps using whatever root it resolves on its own, so two independent regions can each own their overlays.

Props

FloatingPortalRootProvider

The full prop type is FloatingPortalRootProviderProps. Both props are required.
PropTypeDefaultDescription
root
HTMLElement \| null
—
Element floating content is portalled into. Required. null is a valid transient value while the element is still mounting, and suppresses floating content until it resolves.
children
ReactNode
—
Subtree that uses this root. Required.

useFloatingPortalRoot()

import {useFloatingPortalRoot} from '@wearables-ui-toolkit/mrbd';

const portalRoot: HTMLElement | null = useFloatingPortalRoot();
Returns the element overlay content should render into. Resolution order:
  1. The nearest FloatingPortalRootProvider value — including an explicit null, which is honored rather than falling through.
  2. document.body when no provider is present.
  3. null when there is no document, so server rendering is safe.
Call it when you build a custom surface that must live in the same overlay layer as the toolkit popups, and render nothing until it resolves.
import {createPortal} from 'react-dom';
import {useFloatingPortalRoot} from '@wearables-ui-toolkit/mrbd';
import type {ReactNode} from 'react';

function Overlay({children}: {children: ReactNode}) {
  const portalRoot = useFloatingPortalRoot();
  return portalRoot == null ? null : createPortal(children, portalRoot);
}

Behavior

  • Portalled content stays inside the React tree, so context and React event bubbling keep working across the portal boundary. CSS custom properties are the exception — they cascade through the DOM, not the React tree. The design tokens are declared globally, so a portalled surface is themed correctly wherever its root lives, but a token override scoped to one subtree reaches portalled content only when the portal root sits inside that subtree.
  • A popup’s position is measured from its anchor’s viewport rectangle and then re-expressed relative to the portal root: fixed positioning when the root is document.body, and absolute positioning offset by the root’s bounding rectangle otherwise. A scoped root and the document body therefore produce the same on-screen placement.
  • Placement is a preference, not a guarantee. The positioning system flips or clamps a surface to keep it inside the visible boundary, so never lay out content assuming a specific side.
  • Surfaces in the overlay layer are pointer-transparent themselves; only focusable popup content, such as a context menu or a focusable tooltip, takes pointer events back. That is why the layer as a whole can stay pointer-events: none without breaking the popups it hosts.
  • Overlay surfaces are dismissed by Escape and system Back before either reaches route navigation, and focus returns to the control that opened them.
  • Focus navigation treats a popup as its own scope: arrow keys move within the popup, and leaving it reports a dismissal reason instead of jumping into page content behind it.
  • Let App own the portal root in ordinary applications so all overlays share one coordinate and theme context.
  • Create a root only when a host region must clip or own the overlay layer, and keep it pointer-transparent.
  • Read the root through useFloatingPortalRoot instead of querying the DOM.
  • Guard against null before portalling because the root can be unresolved or unavailable during server rendering.

Avoid

  • Fix the underlying layout rather than portalling the toolkit popups into an arbitrary element to escape it.
  • Keep one overlay root per region instead of nesting a second root.
  • Treat DOM structure, generated class names, and stacking values as private implementation details.
  • The overlay layer must remain pointer-transparent; enabling pointer events on it swallows input meant for covered content.