Skip to Content
Version 1AdvancedOverlay Layer

The Overlay Layer and Lui.Portal

LUI renders into two stacked layers: the app tree, and a dedicated overlay layer drawn on top of it. Lui.Portal(key, content) renders content into that overlay layer instead of inline, while the call still lives in your component tree (so it re-renders with your state and unmounts when you stop calling it). This is how overlays draw above all other content and escape any overflow-hidden/Lui.Scroll clipping since UI Toolkit has no z-index, so the only way to paint on top is to render later in a higher layer.

Lui.Portal("my-overlay", Lui.Div("absolute inset-0 ...", panel))

Lui.Portal returns an empty placeholder for the call site and registers the content for the layer. Each portal needs a stable key for reconciliation and stacking order. The overlay layer fills the panel, so portal content is positioned in panel space (absolute + inset-*, or anchored to a trigger, see below). If you emit a portal from inside a Lui.Memo, it is preserved across memo skips automatically; just make sure the overlay’s open/visible state is one of the memo’s dependencies so it refreshes when it changes.

Anchored Overlays: Popup, Tooltip, Context Menu

LuiOverlay ships three overlays that float next to a trigger element. They render through Lui.Portal (so they draw above everything and are never clipped), automatically flip above or below depending on available room, and cap their height plus scroll internally so a long panel stays on screen. Each takes a caller-owned LuiRef (declare it as a field, like a signal) that is attached to the trigger and used to position the panel.

LuiOverlay.Popup: a controlled panel anchored to a trigger. Pass an open bool and a LuiRef; it adds a click-outside backdrop that closes it (toggle off with closeOnClickOutside: false):

private readonly LuiRef _menuAnchor = new(); var trigger = Lui.Button(open.Value ? "Close" : "Open", "...", () => open.Value = !open.Value); LuiOverlay.Popup(trigger, _menuAnchor, open.Value, Lui.Div("flex flex-col gap-1", Lui.Button("Profile", "...", () => { OpenProfile(); open.Value = false; }), Lui.Button("Sign out", "...", () => { SignOut(); open.Value = false; })), onClose: () => open.Value = false)

LuiOverlay.Tooltip: shows a tip on hover. It wires the trigger’s hover via the OnHover node hook for you; you supply a LuiRef and the visibility state:

LuiOverlay.Tooltip( Lui.Div("...", Lui.Text("Hover me")), _tipAnchor, "Tooltips appear on hover and flip to stay on screen.", visible.Value, v => visible.Value = v)

LuiOverlay.ContextMenu: opens a menu on right-click (wired through the OnContext node hook), anchored to the trigger. Items are LuiMenuItem(label, onClick, role, disabled); choosing one closes the menu, and a click-outside backdrop also closes it:

var items = new List<LuiMenuItem> { new LuiMenuItem("Inspect", Inspect), new LuiMenuItem("Rename", Rename, disabled: true), new LuiMenuItem("Delete", Delete, role: LuiModalActionRole.Danger) }; LuiOverlay.ContextMenu(targetNode, _menuAnchor, open.Value, v => open.Value = v, items)

Panels reposition when the trigger or panel changes size; they do not currently follow a trigger that scrolls under them (close the overlay on scroll if you need that). The click-outside backdrop blocks input to the rest of the UI while open, which matches normal menu behaviour.

Toasts

LuiOverlay.ToastHost renders a stack of dismissible notifications pinned to a screen corner. It renders through Lui.Portal, so it is screen-global no matter where you place the call. You own the toast list (add/remove); the host is presentational:

LuiOverlay.ToastHost( toasts.Value, // IReadOnlyList<LuiToast> LuiToastCorner.TopRight, onDismiss: id => RemoveToast(id))

Each LuiToast(id, message, title, tone) is keyed by its id and animates in. The host container is non-blocking (pointer-none) so it never eats input outside the toast cards. Auto-dismiss is the caller’s job — remove a toast from the list on a timer (e.g. from the component’s Update).

Last updated on