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:
- Use the
:if attribute to conditionally render the modal when a specific
live action matches.
- Set the
on_cancel attribute to patch back to the original URL when the
user chooses to close the modal.
- 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:
- Omit the
open attribute in the template.
- 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.