Using A Transient CSS Stylesheet To Remove Scrolling On Body While Modal Is Open
Yesterday, I demonstrated that removing a CSS stylesheet removes its affect on the document. This feature of the DOM (Document Object Model) allows us to apply temporary styles that can be easily toggled on-and-off. At work, I use this technique to temporarily disable scrolling on the body while a modal window is open. This is particularly helpful because CSS properties like overscroll-behavior
don't work on non-scrolling elements.
Run this demo in my JavaScript Demos project on GitHub.
View this code in my JavaScript Demos project on GitHub.
By default, the browser's user-agent applies an implicit "overflow" behavior on the document. This way, when you have a lot of content to deliver, scrollbars render on the side of the window and allow you to move beyond the current viewport. To disable scrolling on the window / body, we can provide our own "overflow" CSS property block:
<style type="text/css">
html,
body {
height: auto ! important ;
overflow: hidden ! important ;
}
</style>
By using the !important
flag, these styles should override any existing height
and overflow
styles, hiding the scrollbar on the window. I include height
here because I sometimes see a strange "jump to top" behavior in certain browsers when scrolling is disabled on a body that has an explicitly-defined height
property.
Now that we have an understanding of how we can disable scrolling on the body, let's combine this set of properties with our transient stylesheet technique in order to only disable scrolling when a modal window is open.
In the following demo, I'm going to take the above <style>
tag and dynamically inject it into the <head>
tag when our modal window is open. Then, when the modal window is closed, I'm going to remove the <style>
tag:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="stylesheet" type="text/css" href="./main.css" />
</head>
<body>
<h1>
Using A Transient CSS Stylesheet To Remove Scrolling On Body While Modal Is Open
</h1>
<button class="open-modal">
Open Modal
</button>
<div class="modal-layout">
<div class="modal">
<h2>
Modal, Yay!
</h2>
<button class="close-modal">
Close Modal
</button>
</div>
</div>
<script type="text/javascript">
// Wire up button event-handlers.
document
.querySelector( ".open-modal" )
.addEventListener( "click", openModal )
;
document
.querySelector( ".close-modal" )
.addEventListener( "click", closeModal )
;
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
/**
* I open the modal window and DISABLE SCROLLING on the body.
*/
function openModal() {
document.querySelector( ".modal-layout" )
.classList
.add( "modal-layout--open" )
;
// When the modal window is open, we want to TURN OFF scrolling on the body
// so that we don't get scroll chaining and / or double scrollbars on the side
// of the window. By prepending this style tag to the head, its "important"
// flags should take precedence over other styles.
var node = document.createElement( "style" );
node.setAttribute( "type", "text/css" );
node.textContent = "html, body { height: auto ! important ; overflow: hidden ! important ; }";
document.head.prepend( node );
}
/**
* I close the modal window and RE-ENALBE SCROLLING on the body.
*/
function closeModal() {
document.querySelector( ".modal-layout" )
.classList
.remove( "modal-layout--open" )
;
// Now that the modal is closed, we want to re-enable scrolling on the body.
// Since it had been disabled by the transient stylesheet, all we have to do
// is remove said stylesheet.
document.head.querySelector( "style" )
?.remove()
;
}
</script>
<!-- To make sure the body is scrolling. -->
<p>1. Lots of copy</p><p>Lots of copy</p><p>Lots of copy</p><p>Lots of copy</p>
<p>2. Lots of copy</p><p>Lots of copy</p><p>Lots of copy</p><p>Lots of copy</p>
<p>3. Lots of copy</p><p>Lots of copy</p><p>Lots of copy</p><p>Lots of copy</p>
<p>4. Lots of copy</p><p>Lots of copy</p><p>Lots of copy</p><p>Lots of copy</p>
<p>5. Lots of copy</p><p>Lots of copy</p><p>Lots of copy</p><p>Lots of copy</p>
<p>6. Lots of copy</p><p>Lots of copy</p><p>Lots of copy</p><p>Lots of copy</p>
<p>7. Lots of copy</p><p>Lots of copy</p><p>Lots of copy</p><p>Lots of copy</p>
</body>
</html>
As you can see, when the modal window is opened, I am dynamically generating the <style>
block and applying it so the DOM using head.prepend()
. Then, when closing the modal window, I query for the first <style>
element in the <head>
and .remove()
it. And, when we run this in the browser, we get the following output:
As you can see, when the modal window is opened, the scrollbars on the window / body disappear. And, what's more, the underlying document retains its current scroll offset. Then, when the modal window is closed - and the transient <style>
tag is removed from the document - the window's scrollbars reappear (again at the previous scroll offset), and I'm free to scroll the document.
Why Not Just Apply a CSS Class to the Document?
You might wonder why I bother with dynamically generating and injecting a <style>
tag - why not just apply a CSS class to the document? It's a good question. And, on its face, it does seem like a simple CSS class would accomplish the same thing. But, the benefit that <style>
tags have is that they can be stacked. Meaning, I can apply multiple, duplicate <style>
tags to the document. And, the effect of the style tags will continue to apply until all stacked stylesheets are removed.
Now, imagine that you have to build a layout that can render multiple modals at a time. Or, maybe one modal and one alert interface. If you attempted to disable scrolling with a CSS class, you might end up adding the same CSS class twice (once for the modal, once for the alert). But, since CSS classes are unique, it only shows up once in the DOM. Then, when you close the alert, and remove the relevant CSS class, you end up removing it for both the modal and the alert, thereby re-enabling scrolling prematurely.
By injecting <style>
tags, you have to "remove" as many as you "inject". This makes the logic more flexible and resilient to complex layouts.
Pro Tip: Developers Should ALWAYS Have Scrollbars Enabled
Unfortunately, when Apple started hiding scrollbars by default, it ruined a whole generation of web developers who forget that not all operating systems work this way. This has lead to an endemic of janky scroll containers (See: Rant). Please, do the world a favor and make sure that your scrollbars are set to always show so that you can foster better empathy with your users.
Want to use code from this post? Check out the license.
Reader Comments
Hey Ben, you thought about using the native DIALOG element for models? Support is good now and been using them instead of DIVs. 'Feels' like the right element to use.
@Dave,
Great minds think alike. I actually have the Mozilla Docs for
<dialog>
open in another tab. I've never used it before; and, at work, I've sort of had to support IE11 for the longest time (not even sure what our Support docs say at this point). But, I am keen to start trying out these newer things.Yeah IE11 and earlier versions of Safari I think are a problem.
If the feature is critical, and you need to support them, would be an issue
Most of the features we use them for aren't mission critical so we just check to see if dialog is supported, if so, add class to the feature so it shows.
The DIALOG has been really easy to style and is consistent across browsers. Definitely give it a try.
Been trying to use more and more of the native stuff lately. HTML and CSS have come a long way. CSS especially.
@Dave,
Totally agree -- I feel like there's a whole host of elements and CSS properties that I haven't been able to use due to IE11 restrictions. I am excited to get "back out there" and see what's available. The dialog is one; I know people have been making a lot of fuss about the summary/detail stuff that GitHub has popularized. Honestly, I could probably due with some sort of course for native HTML.
Hi there! I just wanted to thank you for this code! It's been very helpful as I've been slightly redesigning my website offline. I have a question though...when the modal opens, I want it to have a fade-in in opacity transition effect: "transition: opacity 600ms ease-in-out;", but it's not working for me putting it in the css for "modal-layout--open". So I'm not sure if it has to go somewhere else, or if it's not working because of how the javascript is making the element open up (sorry, I'm not an expert in these things 😓). Any help that you can give would be greatly appreciated!! Thank you again!
@Mark,
Using the CSS
transition
property is great for when something is always visible in the DOM and you are just transitioning from one rendered value to another. It can be tricky when you are applying it to temporary DOM elements where the browser may not have time to render the "initial" state before it applies the transitional state.For "enter" transitions, as a DOM elements appears in the page, I prefer to use the
animation
property. The great thing about theanimation
property is that it doesn't have to go "between states", it only has to go "between keyframes", so it's easier to work with.Going back to a CSS class like
modal--open
, you could have something like this:Now, when you apply the
.modal--open
class to a DOM element, it will start with thefrom
block in the keyframes and then animation the given properties (opacity
in this case) to theto
block using the timing-function.I think you'll have more success with this.
@Mark,
I actually have an older post that explores this exact concept (in the context of "reduced motion", but it has all the same technical details):
www.bennadel.com/blog/4132-applying-multiple-animation-keyframes-to-support-prefers-reduced-motion-in-css.htm
Post A Comment — ❤️ I'd Love To Hear From You! ❤️
Post a Comment →