Modal

Renders a modal dialog for content such as forms and informational panels.

Read more Read less

This component is appropriate for non-critical interactions. For dialogs requiring immediate user response, such as confirmations or warnings, use .alert_dialog/1 instead.

Maturity: Developing

Usage

The dialog is opened with showModal() in one of three ways: from the URL, with the show_modal/1 and hide_modal/1 functions, or with a button that uses the Invoker Commands API.

With URL

To toggle the modal visibility based on the URL:

  1. Use the :if attribute to conditionally render the modal when a specific live action matches.
  2. Set the on_cancel attribute to patch back to the original URL when the user chooses to close the modal.
  3. Set the open attribute to declare the modal's initial visibility state.

Example

<.modal
  :if={@live_action == :show}
  id="pet-modal"
  on_cancel={JS.patch(~p"/pets")}
  open
>
  <:title>Show pet</:title>
  <p>My pet is called Johnny.</p>
  <:footer>
    <.link phx-click={JS.exec("data-cancel", to: "#pet-modal")}>
      Close
    </.link>
  </:footer>
</.modal>

To open the modal, patch or navigate to the URL associated with the live action.

<.link patch={~p"/pets/#{@id}"}>show</.link>

With JS commands

To toggle the modal visibility dynamically:

  1. Omit the open attribute in the template.
  2. Use the show_modal/1 and hide_modal/1 functions to change the visibility.

Example

<.modal id="pet-modal">
  <:title>Show pet</:title>
  <p>My pet is called Johnny.</p>
  <:footer>
    <.link phx-click={JS.exec("data-cancel", to: "#pet-modal")}>
      Close
    </.link>
  </:footer>
</.modal>

To open the modal, use the show_modal/1 function.

<.button
  phx-click={Doggo.show_modal("pet-modal")}
  aria-haspopup="dialog"
>
  show
</.button>

With HTML attributes

command and commandfor are the Invoker Commands API. Unlike the other two ways, this API needs no JavaScript at all.

<.button command="show-modal" commandfor="pet-modal">show</.button>

Both attributes are recent, so the hook handles them if the browser doesn't support them.

Closing

Four things close the dialog, and all of them run on_cancel:

  • the close button the component renders, which uses command="close"
  • Esc and a click outside, unless dismissable is set to false
  • hide_modal/1
  • JS.exec("data-cancel", to: "#pet-modal")

Semantics

The dialog is opened with showModal(), so the browser puts it in the top layer, draws ::backdrop, makes the rest of the document inert and keeps the focus inside. aria-modal is not rendered, because showModal() already marks the component as a modal.

Focus

showModal() moves the focus into the dialog to the first element with the autofocus attribute, or the first focusable element if no element has it.

In a dismissable modal, the close button comes first in the markup, so it receives the focus if no autofocus attribute is present. This is rarely desired.

Set autofocus on the element that should receive the focus:

<.modal id="edit-dog">
  <:title>Edit dog</:title>
  <form>
    <input type="text" name="name" autofocus />
  </form>
</.modal>

The most appropriate element to focus depends on the dialog:

  • If the reader has to work through the content, focus a static element at the top. Opening a modal announces its title and the focused element, but not the body. If a control is focused, the content remains unread until the reader starts looking for it. Focusing a control also scrolls it into view, which can push the beginning of a long body out of sight.
  • If the dialog has focusable elements in the body, such as a form, set the focus to the first such element (e.g. the first input).
  • If the dialog only informs or continues a process, set the focus to the OK or Continue button.
  • If the dialog completes a step that is not easily reversible, set the focus to the least destructive action.

To focus a static element, set both tabindex="-1" and autofocus:

<.modal id="terms">
  <:title>Terms of service</:title>
  <p tabindex="-1" autofocus>Read the following before continuing.</p>
  <h3>Eligibility</h3>
  ...
</.modal>

If the body is short, it can be announced when the modal opens instead by setting aria-describedby to the id of the content element (modal id plus -content suffix):

<.modal id="delete-dog" aria-describedby="delete-dog-content">
  <:title>Delete Bella?</:title>
  <p>This cannot be undone.</p>
</.modal>

A description is announced as a single run of text. Don't set aria-describedby if the content has a structure to navigate, such as a form, a table, or multiple paragraphs.

See also ARIA Authoring Practices.

CSS

A dialog is hidden until it is opened, so no rule is needed for that. Style the backdrop with dialog.modal::backdrop.

Caveats

Setting dismissable={false} removes the close button and renders closedby="none", which leaves no way to dismiss the dialog from the component. Provide your own control in the :footer slot when you do that.

Keyboard

  • Esc - close the dialog, unless dismissable is set to false.

Opening the dialog moves the focus to the first focusable element inside it, and closing it returns the focus to the element that opened it. The focus stays within the dialog while it is open. A dialog with nothing focusable in it leaves the focus outside.

Show pet

Attribute Type Documentation Default Value
class :any

Any additional classes to be added.

Read more Read less
[]

Variations of the component should be expressed via modifier attributes, and it is preferable to use styles on the parent container to arrange components on the page, but if you have to, you can use this attribute to pass additional utility classes to the component.

The value can be a string or a list of strings.

close_label :string

Aria label for the close button. This value should be translated to the

Read more Read less
"Close"

language in which the rest of the page is displayed.

dismissable :boolean

When set to true, the dialog renders a close button and

Read more Read less
true

closedby="any", so that it can also be dismissed with the escape key or by clicking outside it.

Required id * :string
on_cancel %JS{}

An additional Phoenix.LiveView.JS command to execute when the dialog

Read more Read less
%Phoenix.LiveView.JS{ops: []}

is canceled. This command is executed in addition to closing the dialog. If you only want the dialog to be closed, you don't have to set this attribute.

open :boolean

Initializes the modal as open.

false
rest :global

Any additional HTML attributes.

close :slot

The content for the 'close' link. Defaults to the word 'close'.

footer :slot
<:footer>
  <.button autofocus phx-click={JS.exec("data-cancel", to: "#modal-single-not-dismissable")}>
    Close
  </.button>
</:footer>
Required inner_block * :slot

The modal body.

<p>My pet is called Johnny.</p>
Required title * :slot
<:title>Show pet</:title>