Top-layer overlay components — popover, dialog and drawer.
Both use browser-native top-layer primitives (the Popover API and native
<dialog>), so an ancestor's overflow can never clip them. They are
morphdom-safe: the dialog ships JS.ignore_attributes("open") so a
re-render never strips the browser-set open attribute (LiveView #4152),
and every panel carries a stable id so morphdom never replaces the node
(which would force-close top-layer state).
Summary
Functions
JS command that closes the dialog with the given id.
A modal dialog built on the native <dialog> element + showModal().
A drawer / slide-over sheet — a native <dialog> anchored to a viewport edge.
It shares the dialog engine (native showModal() backdrop, focus trap, and
Esc-to-close), so open and close it exactly like a dialog
JS command that opens the dialog with the given id (calls showModal() via the hook).
A popover: a trigger that toggles a top-layer panel positioned next to it.
Functions
JS command that closes the dialog with the given id.
A modal dialog built on the native <dialog> element + showModal().
<.dialog id="confirm">
<:title>Delete project?</:title>
<:subtitle>This cannot be undone.</:subtitle>
<p>All data will be permanently removed.</p>
<:footer>
<.button data-sk-close>Cancel</.button>
<.button variant="danger" phx-click="delete">Delete</.button>
</:footer>
</.dialog>Open it from any element with phx-click={Skua.Components.Overlay.open_dialog("confirm")}.
Native showModal() provides the inert backdrop, focus trap, and Esc-to-close;
JS.ignore_attributes("open") keeps it open across LiveView re-renders.
Attributes
id(:string) (required)class(:any) - Defaults tonil.on_close(Phoenix.LiveView.JS) - Defaults to%Phoenix.LiveView.JS{ops: []}.
Slots
titlesubtitlefooterinner_block(required)
A drawer / slide-over sheet — a native <dialog> anchored to a viewport edge.
It shares the dialog engine (native showModal() backdrop, focus trap, and
Esc-to-close), so open and close it exactly like a dialog:
<.button phx-click={Skua.Components.Overlay.open_dialog("filters")}>Filters</.button>
<.drawer id="filters" side="right">
<:title>Filters</:title>
<:subtitle>Narrow the results.</:subtitle>
<p>…controls…</p>
<:footer>
<.button data-sk-close>Cancel</.button>
<.button variant="primary" data-sk-close>Apply</.button>
</:footer>
</.drawer>side is right (default) | left | top | bottom.
Attributes
id(:string) (required)side(:string) - Defaults to"right". Must be one of"right","left","top", or"bottom".class(:any) - Defaults tonil.on_close(Phoenix.LiveView.JS) - Defaults to%Phoenix.LiveView.JS{ops: []}.
Slots
titlesubtitlefooterinner_block(required)
JS command that opens the dialog with the given id (calls showModal() via the hook).
A popover: a trigger that toggles a top-layer panel positioned next to it.
<.popover id="invite" pad width="280px">
<:trigger><.button>Invite</.button></:trigger>
<p>Panel content…</p>
</.popover>The id must be stable. The trigger is the button (give it a label via
the trigger slot and a look via trigger_variant) — do not nest a
<.button> inside the slot, which would produce invalid nested buttons. It
owns a DOM id and a measurable box and is wired with aria-haspopup/
aria-expanded/aria-controls.
Attributes
id(:string) (required)pad(:boolean) - Defaults tofalse.width(:string) - Defaults tonil.placement(:string) - Defaults to"bottom". Must be one of"bottom", or"right".trigger_variant(:string) - Defaults to"secondary". Must be one of"primary","secondary","ghost", or"danger".class(:any) - Defaults tonil.
Slots
trigger(required)inner_block(required)