61

I didn't find any built in solution or workaround for closing the html5 <dialog> element by clicking on its background(::backdrop), although it's clearly a basic functionality.

Penny Liu
  • 15,447
  • 5
  • 79
  • 98
mantramantramantra
  • 663
  • 1
  • 5
  • 8

11 Answers11

54

Backdrop clicks can be detected using the dialog bounding rect.

var dialog = document.getElementsByTagName('dialog')[0];
dialog.showModal();
dialog.addEventListener('click', function(event) {
  var rect = dialog.getBoundingClientRect();
  var isInDialog = (rect.top <= event.clientY && event.clientY <= rect.top + rect.height &&
    rect.left <= event.clientX && event.clientX <= rect.left + rect.width);
  if (!isInDialog) {
    dialog.close();
  }
});
<dialog>
  <p>Greetings, one and all!</p>
  <form method="dialog">
    <button>OK</button>
  </form>
</dialog>
Lars Flieger
  • 2,421
  • 1
  • 12
  • 34
Seralo
  • 580
  • 6
  • 3
  • 6
    I had to also check that the `event.target` was the `dialog`. EG `if (!isInDialog && event.target.tagName === 'DIALOG')`. If a form is submitted within the dialog via hitting 'enter', then a click event event will bubble up from the submit button, and for some reason its clientX and clientY are 0. – kmurph79 Dec 27 '16 at 01:22
  • 1
    @kmurph79 Try my answer below - it negates the need to check the bounding client rectangle. – marksyzm Aug 30 '19 at 15:31
  • 2
    Any reason for not using `rect.right` and `rect.bottom` instead of `rect.left + rect.width` and `rect.top + rect.height`? – pmiguelpinto90 Jan 06 '20 at 11:07
  • But!, you need to know, that any click by document.querySelector().click() can trigger doalog.close(). Just today faceup with this problem. – AndrewRIGHT Jan 13 '23 at 11:42
  • feels super complicated for what it's doing. – Philll_t Jul 17 '23 at 21:33
  • This does not work properly when there’s a `select` in the `dialog` element. – CWagner Jul 19 '23 at 15:44
24

Another simple method similar to the wrapped div method mentioned by others is to give the dialog itself padding: 0 (as Chrome tends to give the dialog padding, for example) and add the click event to the dialog. The form element picks up the difference anyway so there's no need for an extra div. I've noticed the form wasn't used in any examples above so I thought this would be worth throwing in as it's part of the standard when working with buttons in a dialog.

function onClick(event) {
  if (event.target === dialog) {
    dialog.close();
  }
}

const dialog = document.querySelector("dialog");
dialog.addEventListener("click", onClick);
dialog.showModal();
form {
  max-width: 200px;
}
<button onclick="window.dialog.showModal()">Open dialog</button>

<dialog id="dialog" style="paddinag: 0; border: 0;">
  <form method="dialog">
    <div>Hello from dialog element. You can close me by clicking outside or Close button</div>
    <button>Close</button>
  </form>
</dialog>

Demo on CodePen

Lars Flieger
  • 2,421
  • 1
  • 12
  • 34
marksyzm
  • 5,281
  • 2
  • 29
  • 27
  • 1
    This will work with any element that covers the dialog box; good job including the spec-fulfilling form. – Denis G. Labrecque Jul 18 '20 at 20:33
  • 1
    Very elegant solution. Should be the selected answer. – sayandcode Nov 22 '22 at 14:18
  • 1
    A problem is that when the user for example selects something in the modal and moves his cursor outside of it. The onClick will trigger outside of the modal since the event is fired when the user releases the mouse. – Lars Flieger Jun 22 '23 at 09:12
  • All solutions here suffer from the problem with drag-and-drop. I guess mouse events need to be used instead of click. – silverwind Jul 11 '23 at 16:35
16

Another more efficient solution is to wrap the dialog-content in a div with padding: 0. This way you can check for event.target of the click-event, which references the dialog in case of backdrop and any other element within the div in case of the actual modal.

By not checking the actual dimensions, we can prevent layout cycles.

meaku
  • 897
  • 1
  • 9
  • 14
13

You can't listen to click event on dialog ::backdrop by definition:

The showModal() method of the HTMLDialogElement interface displays the dialog as a modal, over the top of any other dialogs that might be present. It displays into the top layer, along with a ::backdrop pseudo-element. Interaction outside the dialog is blocked and the content outside it is rendered inert.

The semantically appropriate approach to close modal dialog by clicking outside is to inspect the event.target as suggested in this answer and implemented in this answer which can be simplified to

<button onclick="this.nextElementSibling.showModal()">Test</button>

<dialog style="padding: 0" onclick="event.target==this && this.close()">
  <form style="margin: 0; padding: 1rem" method="dialog">
    <p>Clicking the white area doesn't close the dialog</p>
    <button>Click here</button>
  </form>
</dialog>
Jan Turoň
  • 31,451
  • 23
  • 125
  • 169
10

I figured out a very simple solution to this problem. Add a div container inside the <dialog>. Then check if that is the parent of the whole thing. If not, it's the dialog itself and it can be closed.

HTML

<dialog id="favDialog">
  <div class="test">
  Hello
  </div>
</dialog>

JS

document.querySelector('dialog').addEventListener('click', function(e) {
  if(!e.target.closest('div')) {
    e.target.close();
  }
});
Jens Törnell
  • 23,180
  • 45
  • 124
  • 206
  • This is a very good solution because it references the dialog itself instead of making me write other code that references by ID 150 X. – verlager Oct 03 '21 at 02:39
  • 2
    Great solution. Note, however, that the dialog will also close when you click its padding, i.e. the area between the `` border and the `
    ` (in my case `16px` by default). One fix would be to set padding to zero for the `` and add the padding to the `
    ` instead.
    – djvg Nov 07 '22 at 08:22
5

I can't believe no-one mention directly comparing if the clicked element is the same as the dialog, using event.currentTarget:

document.querySelector('dialog').addEventListener('click', event => {
    if (event.target === event.currentTarget) {
        event.currentTarget.close()
    }
})

No calculations, no dialog variables. Just use the event. It has all the information we need...

Works the same for multiple dialogs:

document.querySelectorAll('dialog').forEach(element => 
    element.addEventListener('click', 
        event => (event.target === event.currentTarget) && event.currentTarget.close()
    )
)

To avoid a click inside the dialog from also registering, the dialog{border:0; padding:0;} CSS that every sensible answer mentions should also be set.

Potherca
  • 13,207
  • 5
  • 76
  • 94
3

For anyone stumbling upon this question and wants to follow the solution recommended by @meaku , here's how I solved it to use a to encapsulate the element and not work with getBoundingClientRect() calculation:

const popup = document.getElementById('popup');
const popupDialog = document.getElementById('popup-dialog');
popup.addEventListener('click', function(e){
  console.info(e.target.tagName);
  if (e.target.tagName === 'DIALOG') popupDialog.close()
});
<div id="popup" style="padding: 0">
    <dialog id="popup-dialog" style="display:none;">
        <h4>Dialog Title</h4>
        <footer class="modal-footer">
            <button id="popup-close" type="button">close</button>
            <button id="popup-ok" type="button">ok</button>
        </footer>
    </dialog>
</div>
MiniGod
  • 3,683
  • 1
  • 26
  • 27
Oliver
  • 416
  • 5
  • 14
1

If you use click it will close the dialog even if you drag from inside to outside the dialog, which can be annoying.

You can use pointerdown and pointerup to detect outside clicks more accurately.

// Close a dialog when the user clicks outside of it
// Not using click because form submit can trigger click on submit button,
// and also it would close if you drag from within the dialog to the outside
// (e.g. if you're selecting text, or perhaps you start clicking the close button and reconsider)
function clickOutsideToClose(dialog) {
    function isOutsideDialog(event) {
        const rect = dialog.getBoundingClientRect();
        return (
            event.clientX < rect.left ||
            event.clientX > rect.right ||
            event.clientY < rect.top ||
            event.clientY > rect.bottom
        );
    }
    addEventListener("pointerdown", function (event) {
        if (event.target !== dialog) {
            return;
        }
        if (isOutsideDialog(event)) {
            addEventListener("pointerup", function (event) {
                if (isOutsideDialog(event)) {
                    closeSettings();
                }
            }, { once: true });
        }
    });
}

I haven't tried the div content wrapper method so far. Maybe that also solves this problem, or can be made to solve it?

1j01
  • 3,714
  • 2
  • 29
  • 30
1

An even simpler version would be:

dialog.addEventListener('click', (ev) => {
    if (ev.offsetX < 0 || ev.offsetX > ev.target.offsetWidth ||
        ev.offsetY < 0 || ev.offsetY > ev.target.offsetHeight) {
            dialog.close();
    }
});
1

The following solution is rather simple and does not need any additional html structure:

const dialog = document.getElementById("myDialogId");
dialog.addEventListener("click", (event) => {
  if (event.target.nodeName === "DIALOG") {
    dialog.close();
  }
});

Found on Stefan Judis' blog

HannesT117
  • 103
  • 8
-1

A simplified version of @Seralo answer would be:

dialog.addEventListener("click", event => {
    const rect = dialog.getBoundingClientRect();
    if (event.clientY < rect.top || event.clientY > rect.bottom ||
        event.clientX < rect.left || event.clientX > rect.right) {
        dialog.close();
    }
};
pmiguelpinto90
  • 573
  • 9
  • 19