Skip to content

7. OverlayHost and portals ​

OverlayHost is a portal container for tooltips, popups, modals, dropdowns, toasts, and the dev console. It renders above route screens (last sibling in the screen-space DOM — UI Toolkit has no z-index).

World-space UI (health bars, nameplates) — always UNDER screens, never in this host. World markers belong to 3D objects, so a menu or HUD must be able to cover them — they must never live in the OverlayHost (the top-most layer). Two modes, both below the screens:

  • Variant A (default — SusApp / SusBootstrap.EnsureWorldSpacePanel): a separate panel (SusWorldSpacePanel, PanelSettings.renderMode = WorldSpace) the camera draws under all screen UI.
  • Variant B (fallback, no world-space panel): WorldSpaceService renders flat screen-space markers into a dedicated WorldMarkerLayer — the first child of the screen root (lowest z, below screens and this OverlayHost) — repositioned per frame via WorldToScreenPoint.

See WorldSpaceService in the API reference and 11-api-reference.

SusApp always builds a fixed three-layer scaffold on the UIDocument root — world markers stay under screens, overlays stay on top, and all app content mounts into the middle ScreenHost:

World-space panel (separate UIDocument)          ← variant A, UNDER screens
└── healthbar / nameplate / floating damage

Screen-space UIDocument (root)
├── WorldMarkerLayer  ← first child = lowest (variant-B world markers)
├── ScreenHost        ← app content: Mount<T> component / router SusRouteView
└── OverlayHost       ← last child = always on top of screens
     ├── transition (Transition = 10)
     ├── modal      (Modal      = 20)
     ├── tooltip    (Tooltip    = 30)  ← above modals
     ├── dropdown   (Dropdown   = 40)  ← above modals
     ├── toast      (Toast      = 45)
     └── console    (Console    = 50)  ← topmost

Tooltips and dropdowns sit above modals on purpose: a modal can contain a Select, menu, or control with a hover tooltip — those popups must not paint under the dialog.

Creation ​

AddToOverlay / RemoveFromOverlay are methods on OverlayHost (not on SusComponent). Get the host via SusBootstrap:

csharp
var overlay = SusBootstrap.GetOrCreateOverlay(root);

var popup = BuildMyPopup();
overlay.AddToOverlay(popup, OverlayCategory.Dropdown, dismissOnClickOutside: true);
// ...
overlay.RemoveFromOverlay(popup);

overlay.AddToOverlay(myTooltip, OverlayCategory.Tooltip);
overlay.AddToOverlay(myModal, OverlayCategory.Modal);

OverlayCategory — z-order ​

Higher value = drawn later = on top (no USS z-index). Values from sus-core/Runtime/OverlayCategory.cs:

CategoryValueRole
World0Legacy / internal. World markers do not belong in this host (they'd paint over screens). They render under screens — variant A in a separate SusWorldSpacePanel, variant B in a dedicated WorldMarkerLayer (first child of the root). This value survives only for the deprecated WorldSpaceService.OverlayHost fallback.
Transition10Route transition curtain
Modal20Dialogs, drawers
Tooltip30Hover hints (above modals)
Dropdown40Menus, autocomplete (above modals)
Toast45Snackbars
Console50Dev console / error overlays (topmost)

Within one category, the last added entry is on top (modal stack, toast stack, …).

OverlayEntry ​

csharp
var entry = overlay.AddToOverlay(dialog, OverlayCategory.Modal);
// entry.Element — the dialog itself
// entry.Category — category
// entry.DismissOnClickOutside — false (can be set at creation)
// entry.OnDismiss — callback

Cleaning ​

csharp
overlay.ClearCategory(OverlayCategory.Tooltip);  // remove all tooltips
overlay.ClearAll();                               // remove everything

Convenience methods ​

For common patterns you don't need to position elements by hand:

csharp
// Tooltip anchored to an element (position: "top" | "bottom" | "left" | "right").
// Added to the Tooltip category, dismissOnClickOutside: true.
overlay.ShowTooltip(anchor, tooltipContent, position: "top");

// Dropdown below an anchor; auto-flips above when there's no room. Dropdown category.
overlay.ShowDropdown(anchor, menuContent);

// One-time Escape handler: dismisses the topmost dismissable overlay. Idempotent.
overlay.InstallEscapeHandler();

// Trap Tab / Shift+Tab focus within a modal element.
overlay.InstallFocusTrap(myModalRoot);

Theming (does the theme reach overlays?) ​

Yes. The OverlayHost is the last child of the screen root and receives the full design-token cascade during bootstrap (SusApp / SusBootstrap.GetOrCreateOverlay), plus:

  • SusApp.UseCustomStyles(...) (branding) is applied to the OverlayHost as well as the screen root, so popups match your brand colors too.
  • Typeface reaches the OverlayHost the same way it reaches everything else — through the USS cascade (_font.uss / --sus-font-family-* and the sus-font-* marker classes), not through an explicit apply call. The OverlayHost lives in the same panel as the screen root, so it inherits the panel's typeface and resolves the same marker classes on its own children.
  • SusThemeService.Instance.SetTheme(root, theme) also swaps the .theme-* class on the OverlayHost and on every currently open overlay child, so reparented popups repaint with the active theme.

You normally don't do anything — mount via SusApp and overlays inherit tokens + theme + branding automatically. Manual bootstraps must call SetTheme after building the overlay for it to pick up a non-default theme.

PieceStatusNotes
Dev console (SusConsoleService)ShippedOverlayCategory.Console
SusToast / toast widgetPlannedUse OverlayCategory.Toast / SusToastBase when you build one
World-space healthbar / nameplateDownstream UI packageSeparate world-space panel — under screens, not OverlayHost

API ​

csharp
public class OverlayHost : SusLayer   // SusLayer : VisualElement
{
    public OverlayEntry AddToOverlay(VisualElement element, OverlayCategory category,
        bool dismissOnClickOutside = false, Action onDismiss = null);
    public void RemoveFromOverlay(VisualElement element);
    public void RemoveFromOverlay(OverlayEntry entry);
    public void ClearCategory(OverlayCategory category);
    public void ClearAll();
    public int Count { get; }
    public IReadOnlyList<OverlayEntry> Stack { get; }
    public void DumpStack();   // #if UNITY_EDITOR || DEVELOPMENT_BUILD - diagnostics
}

public class OverlayEntry
{
    public VisualElement Element;
    public OverlayCategory Category;
    public bool DismissOnClickOutside;
    public Action OnDismiss;
}