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 Doggo.JS.show_modal/1 and Doggo.JS.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={Doggo.JS.hide_modal("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 Doggo.JS.show_modal/1 and Doggo.JS.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={Doggo.JS.hide_modal("pet-modal")}>
      Close
    </.link>
  </:footer>
</.modal>

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

<.button
  phx-click={Doggo.JS.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

These close the dialog, and all of them run on_cancel:

  • the close button the component renders, which uses command="close", and Esc, unless closedby is "none"
  • a click outside, if closedby is "any"
  • Doggo.JS.hide_modal/1

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.

Unless closedby is "none", 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 closedby="none" removes the close button, 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 closedby is "none".

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.

closedby :string

How the user can close the dialog. The value is rendered as the

Read more Read less
"any"

dialog's closedby attribute.

  • "any" - the close button, Esc and a click outside.
  • "closerequest" - the close button and Esc, but not a click outside. Use it for a form that should not lose its input to a stray click.
  • "none" - no close button is rendered and neither Esc nor a click outside can close the dialog. You need to provide your own control.
Required id * :string
on_cancel :any

A Phoenix.LiveView.JS command or event name to run when the dialog

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

closes, for example to patch back to the URL the dialog was opened from. This attribute is not required to close the dialog.

open :boolean

Initializes the modal as open.

false
rest :global

Any additional HTML attributes.

footer :slot
<:footer>
  <.button autofocus phx-click={Doggo.JS.hide_modal("modal-single-closedby-closerequest")}>
    Close
  </.button>
</:footer>
Required inner_block * :slot

The modal body.

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