Layered daisyUI <dialog> modal stack driven by a modal_stack
assign. The function component renders the always-visible content
(default slot) plus one <dialog> per stack frame, delegating each
frame's body rendering to the :frame slot the host provides.
The host LV owns state — receiving :opened / :closed / :saved /
:deleted / :dirty PubSub events, pushing/popping the stack, generating
frame_refs. See PhoenixKitProjects.Web.PopupHostLive for the
opinionated wrapper that does this automatically. Use the component
directly when you need full control (e.g. modal-stack alongside other
host state).
Every frame is core's <.modal> (the PkDialog hook: native
<dialog> in the browser's top layer, Esc + backdrop close, stacked
dialogs closing top-first), so a popup here looks and behaves like every
other dialog in the kit. placement picks the classic centered box or
the drawer — a full-height sheet sliding in from the right, the
shape for a create/edit form opened over the page it belongs to.
Slots
:inner_block(default) — the always-visible content. Host typically embeds the root LV here vialive_render(@socket, ...).:frame(with:let={frame}) — per-stack-frame content. Receives the frame map (%{frame_ref, lv, session, id}) so the host can calllive_render(@socket, frame.lv, id: frame.id, session: frame.session).
Attrs
:modal_stack— list of frame maps (ordered bottom→top).:on_close— event name fired on ESC, backdrop-click, and explicit close buttons. Host'shandle_event/3must pop the top frame in response. Defaults to"close_top_modal".:class— outer wrapper class. Defaults to nil (no wrapping).
Stacking and closing
Frames open through showModal(), so the browser's top layer stacks
them in open order — no z-index bookkeeping. Esc and the backdrop push
on_close with the dialog's phx-value-frame-ref, which the host
matches against its top frame (pop_if_top_matches/2). A frame the
host marks closeable: false (a form that has been edited) ignores Esc
and the backdrop — the form's own Cancel is the way out, so a stray
click never eats what was typed. Stack cap at 5 frames matches
PopupHostLive's @max_stack_depth.
Example
<.popup_host modal_stack={@modal_stack} on_close="close_top_modal">
{live_render(@socket, PhoenixKitProjects.Web.OverviewLive,
id: "embed-root",
session: %{
"mode" => "emit",
"pubsub_topic" => @host_topic,
"wrapper_class" => "flex flex-col w-full px-4 py-6 gap-6"
})}
<:frame :let={frame}>
{live_render(@socket, frame.lv, id: frame.id, session: frame.session)}
</:frame>
</.popup_host>
Summary
Functions
Attributes
modal_stack(:list) (required)on_close(:string) - Defaults to"close_top_modal".class(:string) - Defaults tonil.placement(:atom) -:center— the classic box;:end— the drawer (full-height sheet on the right). Defaults to:center. Must be one of:center, or:end.max_width(:string) - core<.modal>max_widthfor every frame ("2xl"suits a form drawer). Defaults to"6xl".modal_box_class(:string) - daisyUImodal-boxsizing/class overrides. Default"w-11/12 max-w-6xl"takes 91% of the viewport width capped atmax-w-6xl(72rem ≈ 1152px) — wider than daisyUI's defaultmax-w-mdso embedded admin LVs (project show, assignment form, etc.) have room for tables + cards + timelines. Pass a different Tailwind size class ("max-w-4xl","max-w-7xl", etc.) if a host page wants a narrower or wider modal.Defaults to
"w-11/12 max-w-6xl".
Slots
inner_block(required)frame(required) - Accepts attributes:any(:any)