Modal Dialog

Open and close `<dialog>` elements, with optional click-outside dismissal and an unsaved-changes warning.

The open and close commands work with <dialog> elements, calling showModal() and close() automatically.

Plain modal

Example: Open and close a modal dialog
<div>
<button _="on click open the next <dialog/>">Open Modal</button>
<dialog>
    <div class="modal-header">
        <h3>Contact Info</h3>
        <button _="on click close the closest <dialog/>">&times;</button>
    </div>
    <input autofocus type="text" placeholder="First Name">
    <input type="text" placeholder="Last Name">
    <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed bibendum a tellus et hendrerit.</p>
</dialog>
</div>

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed bibendum a tellus et hendrerit.

Try It!

the next <dialog/> and the closest <dialog/> target the dialog without needing IDs.

Click outside to close

When a <dialog> is opened with showModal(), the backdrop is part of the dialog element itself - there's no separate backdrop node. Clicks on the dialog content fire with event.target set to the inner element you clicked; clicks on the backdrop fire with event.target equal to the dialog.

So a click-outside handler is just an event filter:

Example: Click the backdrop to dismiss
<div>
<button _="on click open the next <dialog/>">Open Modal</button>
<dialog _="on click[target is me] close me">
    <div class="modal-header">
        <h3>Click outside to close</h3>
        <button _="on click close the closest <dialog/>">&times;</button>
    </div>
    <p>Try clicking on the dimmed backdrop area outside this dialog.</p>
</dialog>
</div>

Try clicking on the dimmed backdrop area outside this dialog.

Try It!

on click[target is me] close me - the [target is me] filter only matches when the click landed directly on the dialog (the backdrop), not on any of its descendants. No coordinate math required.

Warn on unsaved changes

For an editing dialog, you usually want to warn the user before throwing away their changes - but only if they actually made any, and never when they explicitly save. Track dirtiness in an element-scoped variable, route every dismissal path through one custom event, and have the save button bypass it.

Example: Try editing then closing without saving
<div>
<button _="on click open the next <dialog/>">Edit Contact</button>
<dialog _="
    on input from <input,textarea/> in me set :dirty to true

    on closeRequested
      if :dirty
        if confirm('Discard unsaved changes?')
          set :dirty to false then close me
        end
      else
        close me
      end

    on click[target is me] send closeRequested to me
    on cancel halt the event then send closeRequested to me
">
    <div class="modal-header">
        <h3>Edit Contact</h3>
        <button _="on click send closeRequested to closest <dialog/>">&times;</button>
    </div>
    <input autofocus type="text" placeholder="First Name">
    <input type="text" placeholder="Last Name">
    <textarea placeholder="Notes"></textarea>
    <div class="modal-actions">
        <button _="on click send closeRequested to closest <dialog/>">Cancel</button>
        <button class="primary" _="on click
            -- (this is where you'd POST to the server)
            set :dirty to false then close closest <dialog/>">Save</button>
    </div>
</dialog>
</div>

Edit Contact

Try It!

The mental model:

Path Routes through closeRequested? Dirty check fires?
Backdrop click yes yes
Esc key (cancel event) yes yes
X button yes yes
Cancel button yes yes
Save button (success) no - direct close no - :dirty cleared first

Pieces worth calling out: