Skip to content

Overlays

Overlays is not one control but the shared substrate every dismissable surface in this library is built on: OverlayStack/OverlaySession (per-window z-order, cancelable close, Escape and outside-press routing), NaviusOverlayLayer (an in-window Grid portal target replacing WPF’s Popup where window-level input hooks must reach), and NaviusOverlaySurfaceBase (the shared open/close state machine Dialog, AlertDialog, and Drawer derive from). Popover, PreviewCard, and Tooltip do not derive from the base class, but push directly onto OverlayStack and register their Popup-hosted content as an input root, the other consumption pattern this substrate supports.

xmlns:overlay="clr-namespace:Navius.Wpf.Primitives.Controls.OverlaySurface;assembly=Navius.Wpf.Primitives"
<Grid>
<!-- app content -->
<!-- one layer per window, stretched over the root; layer-hosted surfaces
(Dialog/AlertDialog/Drawer) are declared as its direct XAML children -->
<overlay:NaviusOverlayLayer />
</Grid>

A window needs at most one NaviusOverlayLayer, registered the first time it loads (ConditionalWeakTable<Window, NaviusOverlayLayer>). A control that hosts its own popup content in a native Popup (Popover, PreviewCard, Tooltip) does not need a layer at all: it calls OverlayStack.GetFor(window) directly and calls OverlaySession.RegisterInputRoot(popupContent) so Escape and outside-press routing also fire inside the Popup’s own HwndSource.

Property Type Default From Description
Modal bool false OverlayOptions Exposed on the session (OverlaySession.IsModal) for a consumer to decide what modality means visually; the stack itself renders no backdrop.
CloseOnEscape bool true OverlayOptions Escape can close this session, subject to OverlayDismissPolicy.FindEscapeTarget’s topmost-first scan.
CloseOnOutsideClick bool false OverlayOptions A press outside the session’s root (or any registered input root) can close it.
TrapFocus bool false OverlayOptions Tab/Shift+Tab cycle within the session root; focus moves to the first focusable descendant on push.
RestoreFocus bool true OverlayOptions Focus returns to the pre-push focused element on pop, if RestoreFocusTarget was captured (only when TrapFocus was true) and still inside the session’s subtree.
Sessions IReadOnlyList<OverlaySession> - OverlayStack Current sessions for a window, bottommost first.
Topmost OverlaySession? - OverlayStack The topmost session, or null.
Root FrameworkElement - OverlaySession The overlay’s root element, as passed to Push.
StackIndex int - OverlaySession Position within the owning stack; 0 is bottommost.
IsModal / IsClosed bool - OverlaySession Reflect Options.Modal / whether RequestClose already succeeded.
InputRoots IReadOnlyList<FrameworkElement> - OverlaySession Extra roots registered via RegisterInputRoot, whose own PreviewKeyDown/PreviewMouseDown participate in this session’s routing.
Reason OverlayCloseReason - OverlayClosingEventArgs Why the close was requested: EscapeKey, OutsidePress, or Programmatic.
Cancel bool false OverlayClosingEventArgs Set true to keep the session open; blocks the subsequent Closed event.
Method Description
OverlayStack.GetFor(Window) Gets (creating if needed) the per-window stack.
OverlayStack.Push(FrameworkElement, OverlayOptions) Opens a new overlay session on top of the stack; captures RestoreFocusTarget and engages the focus trap when TrapFocus is set.
OverlaySession.RegisterInputRoot(FrameworkElement) Registers an additional popup-tree root (e.g. a Popup’s Child) so Escape and outside-press routing also fire inside its own HwndSource. Safe to call more than once; a no-op after the first.
OverlaySession.RequestClose(OverlayCloseReason) Fires the cancelable Closing event; pops the session and fires Closed unless canceled. Returns whether the session is now closed.
NaviusOverlayLayer.GetFor(Window) The layer registered for a window, or null if none has loaded yet.
OverlayDismissPolicy.FindEscapeTarget(sessions) Pure: topmost-first scan for the first session with CloseOnEscape enabled, skipping sessions above it that opted out.
OverlayDismissPolicy.FindOutsidePressTarget(sessions, isPressInsideRoot) Pure: topmost-first scan for the first session with CloseOnOutsideClick enabled whose root (and every session stacked at or above it) the press falls outside of.
OverlayDismissPolicy.ShouldRestoreFocus(restoreFocusOption, focusIsWithinOverlaySubtree) Pure: true only if RestoreFocus was requested and focus is still somewhere inside the overlay’s subtree at close time.
Command Description
NaviusOverlaySurfaceBase.CloseCommand Static RoutedCommand; content inside a NaviusOverlaySurfaceBase-derived surface (Dialog, AlertDialog, Drawer) binds to it to request a close. Documented per-family on those pages.
Event Signature Fires when
OverlaySession.Closing EventHandler<OverlayClosingEventArgs> RequestClose was called; cancelable.
OverlaySession.Closed EventHandler A close request succeeded (was not canceled); the session is popped from its stack.
NaviusOverlaySurfaceBase.Opened / Closing / Closed see Dialog/AlertDialog/Drawer pages Forwarded 1:1 from the underlying OverlaySession once a layer-hosted surface engages.
Key Behavior
Escape OverlayStack’s window-level PreviewKeyDown hook (and, for any session with registered input roots, the same hook attached directly to each input root) calls OverlayDismissPolicy.FindEscapeTarget, which scans topmost-first and requests a close on the first session with CloseOnEscape enabled.
Tab / Shift + Tab When a session pushes with TrapFocus, OverlayStack.Push sets KeyboardNavigation.TabNavigation/ControlTabNavigation to Cycle on the session’s root and focuses its first focusable descendant.

Outside pointer-down closes via the same window-level (and per-input-root) PreviewMouseDown hook, routed through OverlayDismissPolicy.FindOutsidePressTarget; not a keyboard interaction, but shares the same dismiss-routing path.

No automation peer of its own: NaviusOverlayLayer is a plain, non-focusable, hit-test-transparent- until-active Grid, and OverlayStack/OverlaySession/OverlayOptions/OverlayDismissPolicy are pure C# coordination types with no visual or automation surface. Each concrete family (Dialog, AlertDialog, Drawer, Popover, PreviewCard, Tooltip, Toast) supplies its own AutomationPeer; see their individual pages. The shared routing and focus-trap/restore logic is exercised directly by OverlayStackTests.cs, and indirectly by every family’s own unit suite pushing through the same Push/RequestClose path.

There is no single webEquivalent family to diff against; the deltas below are against the shared web base classes documented in the web contract’s own Overlays notes (OverlayPresence, OverlayPopupBase, OverlayAnchoredPopupBase, OverlayPositionerBase).

  • That inheritance chain (mount lifecycle, dismissable layer, anchored positioning, placement flags all layered on one base) is replaced by two orthogonal WPF pieces instead of one base class: NaviusOverlayLayer (an in-window Grid portal target for layer-hosted surfaces) and OverlayStack/OverlaySession (window-level z-order plus Escape/outside-press routing, consumed directly by Popup-hosted controls via RegisterInputRoot). There is no single shared base spanning both hosting styles.
  • The web’s six cancelable per-phase callbacks (OnOpenAutoFocus, OnCloseAutoFocus, OnEscapeKeyDown, OnPointerDownOutside, OnFocusOutside, OnInteractOutside) collapse into one cancelable OverlaySession.Closing event carrying an OverlayCloseReason. Known residual (M6 audit, first reported against AlertDialog but rooted here and applying to every family built on this substrate): OnOpenAutoFocus has no surviving equivalent at all, not folded into Closing (which only covers the close side) or anywhere else in OverlaySession/ OverlayOptions. Any “N hooks collapsed to one” framing on a consumer’s page therefore overstates what was ported by one hook; a fix would touch every consumer of this substrate, not one family, so it is reported here rather than fixed.
  • The web’s data-starting-style/data-ending-style two-phase forced-reflow enter trick has no equivalent: WPF’s Storyboard.Begin/BeginAnimation do not need it, so consumers drive enter/exit with plain DoubleAnimations keyed off IsOpen instead.
  • Known residual (M6 audit, first reported against Dialog and Drawer but rooted here): a non-modal surface does not restore focus to the pre-open element on close, because OverlayStack.Push only captures RestoreFocusTarget when TrapFocus is true, and every current consumer ties TrapFocus to its own modality flag. The web contract restores focus in both the modal and non-modal paths.

The Overlays Gallery page rendered at the pinned commit, in each theme.

Overlays Gallery page in the light theme

Overlays Gallery page in the dark theme

Overlays Gallery page in the high contrast theme