GPUI.UI.Overlay contains controlled native overlays. Public templates use
ordinary HEEx named slots; internal slot nodes remain an implementation detail.
alias GPUI.UI
alias GPUI.UI.OverlayTooltips
Tooltips accept one arbitrary trigger and one textual content slot:
<Overlay.tooltip id="save-help" delay={250} hoverable={true}>
<:trigger><UI.button id="save" label="Save" /></:trigger>
<:content>Save the current document</:content>
</Overlay.tooltip>delay is measured in milliseconds from 0 through 60_000. Tooltip content
is intentionally textual because the upstream native tooltip owns a separate
view lifecycle.
Popovers
Popovers accept arbitrary GPUI trees in both named slots:
<Overlay.popover
id="account-popover"
label="Account"
open={assigns.account_open}
anchor="top_left"
phx-change="account_open_changed"
>
<:trigger><UI.button id="account-trigger" label="Account" /></:trigger>
<:content>
<div class="w-[220px] p-3">
<text>Account settings</text>
</div>
</:content>
</Overlay.popover>The required label names the popover trigger and its controlled expanded
state. Popover state is controlled by open. Pointer interaction and
Enter/Space can request a state change. Escape and outside clicks dismiss
closable popovers and restore focus. Set closable={false} to disable outside-click dismissal.
Anchors are top_left, top_center, top_right, bottom_left,
bottom_center, bottom_right, left_center, and right_center.
Dialogs
Dialogs are modal and can be opened by a trigger or entirely through assigns:
<Overlay.dialog
id="settings-dialog"
open={assigns.dialog_open}
title="Settings"
width={520}
phx-change="dialog_open_changed"
>
<:trigger><UI.button id="settings-trigger" label="Settings" /></:trigger>
<:content>
<UI.input id="display-name" label="Display name" value={assigns.name} phx-change="name_changed" />
</:content>
</Overlay.dialog>The required non-empty title supplies the dialog heading and names an optional
trigger. The content slot accepts arbitrary GPUI content. Native dialogs expose
a dialog role, trap focus, restore prior focus, and support configurable Escape,
overlay dismissal, close buttons, and width. The trigger slot is optional for programmatic dialogs.
Dropdown menus
Dropdown menus use an arbitrary trigger and repeated textual item slots:
<Overlay.dropdown_menu
id="file-menu"
label="File menu"
open={assigns.file_menu_open}
phx-change="file_menu_open_changed"
phx-select="file_menu_selected"
>
<:trigger><UI.button id="file-trigger" label="File" /></:trigger>
<:item value="new">New file</:item>
<:item value="open" checked={assigns.recent}>Open recent</:item>
<:item value="delete" disabled={true}>Delete</:item>
</Overlay.dropdown_menu>The required label names the menu trigger and its controlled expanded state.
Item values must be non-empty and unique. Selection emits the value through
phx-select; open-state changes use phx-change. The upstream popup menu
provides menu roles, checked state, disabled-item interaction, arrow-key
navigation, Enter confirmation, Escape dismissal, outside-click dismissal, and
focus restoration.
A typical view handles both events explicitly:
def handle_event("file_menu_open_changed", %{value: open}, assigns),
do: {:noreply, %{assigns | file_menu_open: open}}
def handle_event("file_menu_selected", %{value: action}, assigns) do
{:noreply, %{assigns | file_menu_open: false, selected_action: action}}
endControlled reconciliation
Elixir assigns remain authoritative, while persistent native registry entries preserve transient state between snapshots. A local interaction is applied immediately and recorded until the corresponding controlled snapshot arrives. Stale snapshots are ignored rather than visibly undoing the interaction.
Removing an overlay from the tree removes its registry entry. Stable IDs must not be reused for a different component kind in the same tree.