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:
- 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={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:
- Omit the
open attribute in the template.
- 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.