Modals
Dialog on the panel surface with a proper scrim. Escape and click-away close it, and open/close are plain LiveView.JS commands - no routes required.
Invite dialog
The whole wiring in one place: a modal rendered with hide stays out of view until show_modal/1 - a plain LiveView.JS command on any phx-click, no server round-trip - and hide_modal/1 closes it the same way. Escape and click-away close by default (close_on_escape / close_on_click_away), title labels the dialog for screen readers, max_width sizes the box, and the :footer slot puts the actions in the band at the bottom.
Share this link with your teammates and they'll join the workspace with member access.
<.button color="gray" variant="outline" phx-click={show_modal("showcase-modal")}>
Open modal
</.button>
<.modal id="showcase-modal" title="Invite your team" hide max_width="sm">
<p class="text-sm text-gray-500 dark:text-gray-400">
Share this link with your teammates and they'll join the workspace
with member access.
</p>
<div class="mt-4">
<.input_group>
<.input type="text" name="invite_url" value="https://example.com/join/x1y2z3" readonly />
<:trailing><kbd><span>⌘</span>C</kbd></:trailing>
</.input_group>
</div>
<:footer>
<.button color="gray" variant="outline" phx-click={hide_modal("showcase-modal")}>
Cancel
</.button>
<.button phx-click={hide_modal("showcase-modal")}>Copy link</.button>
</:footer>
</.modal>
A form in a modal
The most common thing a dialog holds. Fields go in the content inside a form, the submit row goes in the :footer, and the button reaches back into the form with the HTML form attribute - the footer band is outside the form element, and form="..." is how a submit crosses that boundary. On a narrow screen the buttons stack with the primary one on top, under the thumb, and unstack to a right-aligned row from sm up.
Make changes to your profile here. Nothing saves until you say so.
<.button color="gray" variant="outline" phx-click={show_modal("showcase-modal-form")}>
<.icon name="hero-pencil-square" class="w-4 h-4 mr-1" /> Edit profile
</.button>
<.modal id="showcase-modal-form" title="Edit profile" hide max_width="md">
<p class="text-sm text-gray-500 dark:text-gray-400">
Make changes to your profile here. Nothing saves until you say so.
</p>
<form id="showcase-profile-form" phx-submit={hide_modal("showcase-modal-form")}>
<div class="flex flex-col gap-4 mt-4">
<.field type="text" name="profile_name" value="Alex Rivera" label="Name" />
<.field type="text" name="profile_username" value="@alexrivera" label="Username" />
<.field
type="textarea"
name="profile_bio"
value=""
label="Bio"
placeholder="A line about you"
/>
</div>
</form>
<:footer>
<.button color="gray" variant="outline" phx-click={hide_modal("showcase-modal-form")}>
Cancel
</.button>
<%!-- The footer band sits outside the <form> element, so the submit
reaches back in with the form attribute - without it this button
could never submit the fields above it. --%>
<.button type="submit" form="showcase-profile-form">
Save changes
</.button>
</:footer>
</.modal>
A long body, a footer that stays put
The box is a column: the header and the footer are fixed bands and the content between them is the only part that scrolls. Give the modal more text than the viewport can hold and the body scrolls under the action row rather than pushing it off the bottom, so the way out is always on screen.
Section 1. You agree to use the service in the way a reasonable person would, and we agree to keep it running. Neither of us will sell the other's data, send unsolicited mail, or pretend a change to this document is a feature announcement. Scroll on: the accept button is not going anywhere.
Section 2. You agree to use the service in the way a reasonable person would, and we agree to keep it running. Neither of us will sell the other's data, send unsolicited mail, or pretend a change to this document is a feature announcement. Scroll on: the accept button is not going anywhere.
Section 3. You agree to use the service in the way a reasonable person would, and we agree to keep it running. Neither of us will sell the other's data, send unsolicited mail, or pretend a change to this document is a feature announcement. Scroll on: the accept button is not going anywhere.
Section 4. You agree to use the service in the way a reasonable person would, and we agree to keep it running. Neither of us will sell the other's data, send unsolicited mail, or pretend a change to this document is a feature announcement. Scroll on: the accept button is not going anywhere.
Section 5. You agree to use the service in the way a reasonable person would, and we agree to keep it running. Neither of us will sell the other's data, send unsolicited mail, or pretend a change to this document is a feature announcement. Scroll on: the accept button is not going anywhere.
Section 6. You agree to use the service in the way a reasonable person would, and we agree to keep it running. Neither of us will sell the other's data, send unsolicited mail, or pretend a change to this document is a feature announcement. Scroll on: the accept button is not going anywhere.
Section 7. You agree to use the service in the way a reasonable person would, and we agree to keep it running. Neither of us will sell the other's data, send unsolicited mail, or pretend a change to this document is a feature announcement. Scroll on: the accept button is not going anywhere.
Section 8. You agree to use the service in the way a reasonable person would, and we agree to keep it running. Neither of us will sell the other's data, send unsolicited mail, or pretend a change to this document is a feature announcement. Scroll on: the accept button is not going anywhere.
Section 9. You agree to use the service in the way a reasonable person would, and we agree to keep it running. Neither of us will sell the other's data, send unsolicited mail, or pretend a change to this document is a feature announcement. Scroll on: the accept button is not going anywhere.
Section 10. You agree to use the service in the way a reasonable person would, and we agree to keep it running. Neither of us will sell the other's data, send unsolicited mail, or pretend a change to this document is a feature announcement. Scroll on: the accept button is not going anywhere.
Section 11. You agree to use the service in the way a reasonable person would, and we agree to keep it running. Neither of us will sell the other's data, send unsolicited mail, or pretend a change to this document is a feature announcement. Scroll on: the accept button is not going anywhere.
Section 12. You agree to use the service in the way a reasonable person would, and we agree to keep it running. Neither of us will sell the other's data, send unsolicited mail, or pretend a change to this document is a feature announcement. Scroll on: the accept button is not going anywhere.
<.button color="gray" variant="outline" phx-click={show_modal("showcase-modal-terms")}>
Read the terms
</.button>
<.modal id="showcase-modal-terms" title="Terms of service" hide max_width="md">
<div class="flex flex-col gap-4 text-sm text-gray-500 dark:text-gray-400">
<p :for={n <- 1..12}>
Section {n}. You agree to use the service in the way a reasonable
person would, and we agree to keep it running. Neither of us will
sell the other's data, send unsolicited mail, or pretend a change
to this document is a feature announcement. Scroll on: the accept
button is not going anywhere.
</p>
</div>
<:footer>
<.button color="gray" variant="outline" phx-click={hide_modal("showcase-modal-terms")}>
Decline
</.button>
<.button phx-click={hide_modal("showcase-modal-terms")}>Accept</.button>
</:footer>
</.modal>
Sizes
max_width caps the box: sm, md (the default), lg, xl, 2xl and full. The wrapper keeps its own padding either way, so even full stays off the viewport edges on a phone. Pick the smallest one the content reads comfortably in - a two-line confirmation in a 2xl box is mostly empty space.
This box is capped at max_width="sm". Everything else about it is the same dialog.
This box is capped at max_width="md". Everything else about it is the same dialog.
This box is capped at max_width="lg". Everything else about it is the same dialog.
This box is capped at max_width="xl". Everything else about it is the same dialog.
This box is capped at max_width="2xl". Everything else about it is the same dialog.
This box is capped at max_width="full". Everything else about it is the same dialog.
<div class="flex flex-wrap gap-2">
<.button
:for={size <- ~w(sm md lg xl 2xl full)}
color="gray"
variant="outline"
phx-click={show_modal("showcase-modal-size-" <> size)}
>
{size}
</.button>
</div>
<.modal
:for={size <- ~w(sm md lg xl 2xl full)}
id={"showcase-modal-size-" <> size}
title={"Size " <> size}
hide
max_width={size}
>
<p class="text-sm text-gray-500 dark:text-gray-400">
This box is capped at max_width="{size}". Everything else about it is
the same dialog.
</p>
<:footer>
<.button
color="gray"
variant="outline"
phx-click={hide_modal("showcase-modal-size-" <> size)}
>
Close
</.button>
</:footer>
</.modal>
A footer with a tertiary action
The band is a flex row that pushes its children to the end. Want something on the far left instead - a help link, a destructive action, a step counter - put one full-width child in the slot and lay it out yourself. The band, the border and the padding stay the component's job.
Headerless confirmation
Confirmation dialogs often center their heading in the content instead of a header bar. hide_header hides the bar visually but keeps it in the DOM, so the title you pass still names the dialog for screen readers via aria-labelledby. It also drops the corner close button, so give the content its own way out (here a button, plus escape and click-away).
Unsubscribed successfully
You won't get marketing emails from us anymore. You can resubscribe any time from your notification settings.
<.button color="gray" variant="outline" phx-click={show_modal("showcase-modal-headerless")}>
Open confirmation
</.button>
<.modal
id="showcase-modal-headerless"
title="Unsubscribed successfully"
hide
hide_header
max_width="sm"
>
<div class="text-center">
<div class="flex items-center justify-center w-12 h-12 mx-auto rounded-full bg-success-100 dark:bg-success-500/15">
<.icon name="hero-check" class="w-6 h-6 text-success-600 dark:text-success-400" />
</div>
<h3 class="mt-3 text-lg font-semibold text-gray-800 dark:text-gray-200">
Unsubscribed successfully
</h3>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
You won't get marketing emails from us anymore. You can resubscribe
any time from your notification settings.
</p>
<.button
class="mt-6"
color="gray"
variant="outline"
phx-click={hide_modal("showcase-modal-headerless")}
>
Back to settings
</.button>
</div>
</.modal>
Properties
| Attribute | Type | Default | Description |
|---|---|---|---|
class
|
any |
nil
|
modal class |
close_modal_target
|
string |
nil
|
close_modal_target allows you to target a specific live component for the close event to go to. eg: close_modal_target={@myself} |
close_on_click_away
|
boolean |
true
|
whether the modal should close when a user clicks away |
close_on_escape
|
boolean |
true
|
whether the modal should close when a user hits escape |
hide
|
boolean |
false
|
modal is hidden |
hide_close_button
|
boolean |
false
|
whether or not the modal should have a close button in the header |
hide_header
|
boolean |
false
|
visually hides the header bar (screen-reader-only, not removed) for confirmation-style modals that carry their heading in the content. Pass a real title anyway - it stays in the DOM as the dialog's accessible name via aria-labelledby. Implies no close button, so give the content its own way to close |
id
|
string |
"modal"
|
modal id |
max_width
|
string |
"md"
|
modal max width
one of: "sm", "md", "lg", "xl", "2xl", "full"
|
on_cancel
|
{:struct, Phoenix.LiveView.JS} |
%Phoenix.LiveView.JS{ops: [["exec", %{attr: "data-cancel-default"}]]}
|
a JS function to execute when the modal is closed. Defaults to pushing close_modal event |
on_open
|
{:struct, Phoenix.LiveView.JS} |
%Phoenix.LiveView.JS{ops: []}
|
additional JS commands to run when the modal opens |
rest
|
global |
||
title
|
string |
nil
|
modal title |
:footer
slot
|
slot |
the action row the modal owns: pinned below the content, which scrolls under it, and wearing the same band the table's tfoot and the alert dialog's footer wear. Buttons stack on mobile and unstack to a right-aligned row at sm. Omit it and nothing renders - put the buttons in the content the way you always did | |
:inner_block
slot
|
slot |