HTML `dialog` element: scroll content independently of background
Asked Answered
Z

5

5

I am trying to use the dialog element.

When the dialog/modal is closed, the body should be scrollable.

When the dialog/modal is open, if it has large contents, the dialog/modal should be scrollable.

However, when the dialog/modal is open, I don't want scroll to apply to both the dialog/modal and the body background, which is what it seems to do by default.

Example: https://output.jsbin.com/mutudop/3.

How can I make scroll apply only to the dialog/modal contents, when the dialog/modal is open?

Note: I am only interested in solutions using the native dialog element.

Zoller answered 4/5, 2019 at 4:43 Comment(1)
Hey, I think some feedback would be nice. As far as I can see it you haven't responded to 2 answers so far. They need to know if their answers solve your question or if the answer/your question is/isn't relevant (anymore).Rau
S
2

So I tried it as well and came up with this:

(function() {
  var openBtn = document.querySelector("button#open");
  var myDialog = document.querySelector("dialog");

  openBtn.addEventListener('click', function() {
    if (typeof myDialog.showModal === "function") {
      myDialog.showModal();
      document.querySelector("body").classList.add("overflow-hidden");
    } else {
      alert("Dialog API not supported by browser");
    }
  });
})();
* {
  box-sizing: border-box;
}

.wrapper {
  height: 10000px;
}

dialog {
  width: 100%;
  height: 100%;
  padding: 0;
  margin: 0;
  border: 0;
  z-index: 100;
  background: transparent;
  overflow-y: auto;
}

dialog>div {
  width: 50%;
  height: 500px;
  background: white;
  border: 3px solid black;
  margin: 0 auto;
  margin-top: 50px;
}

.overflow-hidden {
  overflow: hidden;
}
<div class="wrapper">
  <dialog>
    <div>
      <form method="dialog">
        <button onclick='document.body.classList.remove("overflow-hidden");' value="cancel">Cancel</button>
      </form>
    </div>
  </dialog>

  <button id="open">Open Dialog</button>
  <h4>You can scroll the body now but not when the dialog is opened.</h4>
</div>

You might have noticed that I added two lines of JS to hide/show the overflow of the body and you will probably need them as you can't target the body with pure CSS if you want to check if the dialog is opened or not.

If you don't want them you can remove them and it just works fine. However, you will have two scroll bars on the right side. This is how it looks without the JS:

(function() {
  var openBtn = document.querySelector("button#open");
  var myDialog = document.querySelector("dialog");

  openBtn.addEventListener('click', function() {
    if (typeof myDialog.showModal === "function") {
      myDialog.showModal();
    } else {
      alert("Dialog API not supported by browser");
    }
  });
})();
* {
  box-sizing: border-box;
}

.wrapper {
  height: 10000px;
}

dialog {
  width: 100%;
  height: 100%;
  padding: 0;
  margin: 0;
  border: 0;
  z-index: 100;
  background: transparent;
  overflow-y: auto;
}

dialog>div {
  width: 50%;
  height: 500px;
  background: white;
  border: 3px solid black;
  margin: 0 auto;
  margin-top: 50px;
}

.overflow-hidden {
  overflow: hidden;
}
<div class="wrapper">
  <dialog>
    <div>
      <form method="dialog">
        <button value="cancel">Cancel</button>
      </form>
    </div>
  </dialog>

  <button id="open">Open Dialog</button>
</div>

If you need any explanation let me know but I believe the code should be self-explanatory.

Stacte answered 8/5, 2019 at 17:48 Comment(1)
This answer doesn't take the escape key into account. See my answer for more information.Gelatinize
I
13

2023/2024 Solution (No Javascript / CSS Only)

It is now possible to achieve this with CSS :has selector.

Simply add the following CSS:

body:has(dialog[open]) {
  overflow: hidden;
}

If you are using tailwindcss:

<body className="[&:has(dialog[open])]:overflow-hidden">

The above CSS will remove the scrolling from the body when the dialog element is open.

Currently every major browser supports the :has selector. Check the detailed compatibility here.

.long {
  height: 500vh;
  background-color: rebeccapurple;
}

body:has(dialog[open]) {
  overflow: hidden;
}
<div class="long">
  <button onclick="document.querySelector('dialog').showModal()">open dialog</button>
</div>

<dialog>
  Hello world!
</dialog>
Immoderacy answered 27/12, 2023 at 14:38 Comment(1)
Thanks! I try both Tailwind and CSS, they work very well!Toffic
S
2

So I tried it as well and came up with this:

(function() {
  var openBtn = document.querySelector("button#open");
  var myDialog = document.querySelector("dialog");

  openBtn.addEventListener('click', function() {
    if (typeof myDialog.showModal === "function") {
      myDialog.showModal();
      document.querySelector("body").classList.add("overflow-hidden");
    } else {
      alert("Dialog API not supported by browser");
    }
  });
})();
* {
  box-sizing: border-box;
}

.wrapper {
  height: 10000px;
}

dialog {
  width: 100%;
  height: 100%;
  padding: 0;
  margin: 0;
  border: 0;
  z-index: 100;
  background: transparent;
  overflow-y: auto;
}

dialog>div {
  width: 50%;
  height: 500px;
  background: white;
  border: 3px solid black;
  margin: 0 auto;
  margin-top: 50px;
}

.overflow-hidden {
  overflow: hidden;
}
<div class="wrapper">
  <dialog>
    <div>
      <form method="dialog">
        <button onclick='document.body.classList.remove("overflow-hidden");' value="cancel">Cancel</button>
      </form>
    </div>
  </dialog>

  <button id="open">Open Dialog</button>
  <h4>You can scroll the body now but not when the dialog is opened.</h4>
</div>

You might have noticed that I added two lines of JS to hide/show the overflow of the body and you will probably need them as you can't target the body with pure CSS if you want to check if the dialog is opened or not.

If you don't want them you can remove them and it just works fine. However, you will have two scroll bars on the right side. This is how it looks without the JS:

(function() {
  var openBtn = document.querySelector("button#open");
  var myDialog = document.querySelector("dialog");

  openBtn.addEventListener('click', function() {
    if (typeof myDialog.showModal === "function") {
      myDialog.showModal();
    } else {
      alert("Dialog API not supported by browser");
    }
  });
})();
* {
  box-sizing: border-box;
}

.wrapper {
  height: 10000px;
}

dialog {
  width: 100%;
  height: 100%;
  padding: 0;
  margin: 0;
  border: 0;
  z-index: 100;
  background: transparent;
  overflow-y: auto;
}

dialog>div {
  width: 50%;
  height: 500px;
  background: white;
  border: 3px solid black;
  margin: 0 auto;
  margin-top: 50px;
}

.overflow-hidden {
  overflow: hidden;
}
<div class="wrapper">
  <dialog>
    <div>
      <form method="dialog">
        <button value="cancel">Cancel</button>
      </form>
    </div>
  </dialog>

  <button id="open">Open Dialog</button>
</div>

If you need any explanation let me know but I believe the code should be self-explanatory.

Stacte answered 8/5, 2019 at 17:48 Comment(1)
This answer doesn't take the escape key into account. See my answer for more information.Gelatinize
G
0

This answer takes the escape key into account. I add a keydown event listener to document.documentElement rather than the actual dialog elements. This is because when a dialog has a keydown event listener, it doesn't always fire. For example, if a dialog is open and a button inside of it has focus and you push the escape key, the keydown event listener will fire. But let's suppose that the dialog has some text in it and you highlight the text and then push the escape key. In this scenario, the keydown event listener will not fire.

const activeModals = [];

function openModal(dialogSelector) {
  const dialog = document.querySelector(dialogSelector);
  dialog.showModal();
  activeModals.push(dialog);
  document.body.classList.add('overflow-hidden');
}

function closeActiveModal() {
  const activeModal = activeModals.pop();
  activeModal.close();

  if (activeModals.length === 0) {
    document.body.classList.remove('overflow-hidden');
  }
}

document.documentElement.addEventListener('keydown', (e) => {
  if (e.key === 'Escape' && activeModals.length) {
    e.preventDefault();
    closeActiveModal();
  }
});

document.querySelectorAll('[data-toggle="modal"]').forEach((button) => {
  button.addEventListener('click', () => {
    openModal(button.getAttribute('data-target'));
  });
});

document.querySelectorAll('[data-dismiss="modal"]').forEach((button) => {
  button.addEventListener('click', closeActiveModal);
});

let fillerHtml = '';

for (let i = 1; i <= 100; i++) {
  fillerHtml += `<p>${i}</p>`;
}

document.querySelectorAll('.filler').forEach((div) => {
  div.innerHTML = fillerHtml;
});
.overflow-hidden {
  overflow: hidden;
}

p {
  font-size: 20px;
}
<button data-toggle="modal" data-target="#dialog1">Open Dialog 1</button>

<dialog id="dialog1">
  <h1>Dialog 1</h1>
  <button data-dismiss="modal">Close Dialog 1</button>
  <button data-toggle="modal" data-target="#dialog2">Open Dialog 2</button>
  <div class="filler"></div>
</dialog>

<dialog id="dialog2">
  <h1>Dialog 2</h1>
  <button data-dismiss="modal">Close Dialog 2</button>
</dialog>

<div class="filler"></div>
Gelatinize answered 16/4, 2021 at 2:6 Comment(0)
P
-1

Update

I created another example where your main content is not scrolled with your modal if it is larger than your main content. You can set position to fixed on your container to achieve this.

(function() {
  var openBtn = document.getElementById('open-dialog');
  var myDialog = document.getElementById('my-dialog');

  openBtn.addEventListener('click', function() {
    if (typeof myDialog.showModal === "function") {
      myDialog.showModal();
    } else {
      alert("Dialog API not supported by browser");
    }
  });

})();
#container {
  height: 100vh;
  width: 100vw;
  position: fixed;
  top: 0;
  left: 0;
  background: #ccc;
}

#my-dialog {
  margin-top: 1rem;
  margin-bottom: 3rem;
  top: 3rem;
  width: 50%;
  overflow-y: auto;
}

#my-dialog__content {
  display: flex;
  flex-direction: column;
  height: 200vh;
}

menu {
  width: 100%;
  padding: 0;
  margin: 0 auto;
}

#cancel-button {
  width: 100%
}
<div id="container">
  <dialog id="my-dialog">
    <div id="my-dialog__content">
      <form method="dialog">
        <menu>
          <button id="cancel-button" value="cancel">Cancel</button>
        </menu>
      </form>
    </div>
  </dialog>

  <menu>
    <button id="open-dialog">Open Dialog</button>
  </menu>
</div>

Original answer

You can set a max-height on your dialog and style the contents of your dialog accordingly. See example below.

(function() {
  var openBtn = document.getElementById('open-dialog');
  var myDialog = document.getElementById('my-dialog');

  openBtn.addEventListener('click', function() {
    if (typeof myDialog.showModal === "function") {
      myDialog.showModal();
    } else {
      alert("Dialog API not supported by browser");
    }
  });

})();
#my-dialog {
  width: 50%;
  max-height: 50vh;
  overflow-y: auto;
}

#my-dialog__content {
  display: flex;
  flex-direction: column;
  height: 150vh;
}

menu {
  width: 100%;
  padding: 0;
  margin: 0 auto;
}

#cancel-button {
  width: 100%
}
<div id="container">
  <dialog id="my-dialog">
    <div id="my-dialog__content">
      <form method="dialog">
        <menu>
          <button id="cancel-button" value="cancel">Cancel</button>
        </menu>
      </form>
    </div>
  </dialog>

  <menu>
    <button id="open-dialog">Open Dialog</button>
  </menu>
</div>
Pickings answered 4/5, 2019 at 7:54 Comment(3)
Very interesting. What if I want scrolling to work not inside the modal but on the outside i.e. scroll the overlay? You can see the desired effect if you go to unsplash.com/photos/bQ-XtB9IWmo?force_page_modal.Zoller
I see. I just updated my answer. Let me know if this is what you were after.Pickings
That works, but it doesn't preserve scrolling of the modal's background when the modal is closed. In my original example, the modal background has a large amount of content, which should be scrollable but only when the modal is closed. Sorry I didn't clarify this in my original post. I'll update my question.Zoller
P
-1

Simple solution is : Once the mnodel is displayed make a one more DIV as overlay which covers full screen, in that place css { pointer-events:none} and model will be placed on top of that. user can not click on body content other than model data.

I have created sample: http://jsfiddle.net/z3sgvnox/

<body id="content-body">


<div id="container">
  <dialog id="my-dialog">
    <div id="my-dialog__content">
      <form method="dialog">
        <menu>
          <button id="cancel-button" value="cancel">Cancel</button>
        </menu>
      </form>
    </div>
  </dialog>

  <menu>
    <button id="open-dialog">Open Dialog</button>
  </menu>
</div>
</body>

CSS

#container {
  height: 100vh;
  width: 100vw;
  position: fixed;
  top: 0;
  left: 0;
  background: #ccc;
}

#my-dialog {
  margin-top: 1rem;
  margin-bottom: 3rem;
  width: 50%;
  overflow-y: auto;
      max-height: 80%;
}
.hideScroll{
  overflow:hidden;
  pointer-events:none;
}

#my-dialog__content {
  display: flex;
  flex-direction: column;
  height: 200vh;
}

menu {
  width: 100%;
  padding: 0;
  margin: 0 auto;
}

#cancel-button {
  width: 100%
}

JS:

(function() {
  var openBtn = document.getElementById('open-dialog');
  var myDialog = document.getElementById('my-dialog');
var bodyData = document.getElementById('content-body');
  openBtn.addEventListener('click', function() {
    if (typeof myDialog.showModal === "function") {
      myDialog.showModal();
      bodyData.classList.add("hideScroll");
    } else {
      alert("Dialog API not supported by browser");
    }
  });

})();
Pilcomayo answered 8/5, 2019 at 9:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.