Prevent body scrolling but allow overlay scrolling
Theory
Looking at current implementation of the pinterest site (it might change in the future), when you open the overlay, a noscroll
class is applied to the body
element (setting overflow: hidden
) making the body
no longer scrollable.
The overlay created on-the-fly or already injected in the page and made visible via display: block
— it makes no difference – has position : fixed
and overflow-y: scroll
, with top
, left
, right
and bottom
properties set to 0
: this style makes the overlay fill the whole viewport (but now we are in 2022, so you may use inset: 0
instead).
The div
inside the overlay is in position: static
so the vertical scrollbar is related to that element. This is resulting in a scrollable but fixed overlay.
When you close the overlay, you have to hide it (using display: none
) and you could even remove the node via javascript (or just the content inside, it's up to you but also depends on the nature of the content).
The final step is to also remove the noscroll
class applied to the body
(so the overflow
property gets back to the value it had previously)
Code
Codepen Example
(it works by changing the aria-hidden
attribute of the overlay in order to show and hide it and to increase its accessibility).
Markup
(open button)
<button type="button" class="open-overlay">OPEN LAYER</button>
(overlay and close button)
<section class="overlay" aria-hidden="true" tabindex="-1">
<div>
<h2>Hello, I'm the overlayer</h2>
...
<button type="button" class="close-overlay">CLOSE LAYER</button>
</div>
</section>
CSS
.noscroll {
overflow: hidden;
}
.overlay {
position: fixed;
overflow-y: scroll;
inset: 0; }
[aria-hidden="true"] { display: none; }
[aria-hidden="false"] { display: block; }
Javascript (vanilla-JS)
var body = document.body,
overlay = document.querySelector('.overlay'),
overlayBtts = document.querySelectorAll('button[class$="overlay"]'),
openingBtt;
[].forEach.call(overlayBtts, function(btt) {
btt.addEventListener('click', function() {
/* Detect the button class name */
var overlayOpen = this.className === 'open-overlay';
/* storing a reference to the opening button */
if (overlayOpen) {
openingBtt = this;
}
/* Toggle the aria-hidden state on the overlay and the
no-scroll class on the body */
overlay.setAttribute('aria-hidden', !overlayOpen);
body.classList.toggle('noscroll', overlayOpen);
/* On some mobile browser when the overlay was previously
opened and scrolled, if you open it again it doesn't
reset its scrollTop property */
overlay.scrollTop = 0;
/* forcing focus for Assistive technologies but note:
- if your modal has just a phrase and a button move the
focus on the button
- if your modal has a long text inside (e.g. a privacy
policy) move the focus on the first heading inside
the modal
- otherwise just focus the modal.
When you close the overlay restore the focus on the
button that opened the modal.
*/
if (overlayOpen) {
overlay.focus();
}
else {
openingBtt.focus();
openingBtt = null;
}
}, false);
});
/* detect Escape key when the overlay is open */
document.body.addEventListener('keyup', (ev) => {
if (ev.key === "Escape" && overlay.getAttribute('aria-hidden') === 'false') {
overlay.setAttribute('aria-hidden', 'true');
body.classList.toggle('noscroll', false);
openingBtt.focus();
openingBtt = null;
}
})
Finally, here's another example in which the overlay opens with a fade-in effect by a CSS transition
applied to the opacity
property. Also a padding-right
is applied to avoid a reflow on the underlying text when the scrollbar disappears.
Codepen Example (fade)
CSS
.noscroll { overflow: hidden; }
@media (min-device-width: 1025px) {
/* not strictly necessary, just an experiment for
this specific example and couldn't be necessary
at all on some browser */
.noscroll {
padding-right: 15px;
}
}
.overlay {
position: fixed;
overflow-y: scroll;
inset: 0;
}
[aria-hidden="true"] {
transition: opacity 1s, z-index 0s 1s;
width: 100vw;
z-index: -1;
opacity: 0;
}
[aria-hidden="false"] {
transition: opacity 1s;
width: 100%;
z-index: 1;
opacity: 1;
}
Prevent overscrolling of web page
The accepted solution was not working for me. The only way I got it working while still being able to scroll is:
html {
overflow: hidden;
height: 100%;
}
body {
height: 100%;
overflow: auto;
}
Prevent overflow / rubberband scrolling on iOS
The solution:
Type 1:
The most basic solution to prevent overflow scrolling on the element itself is to prevent default on touch events.
document.body.addEventListener('touchmove', function(e) {
e.preventDefault();
});
This method however disables the browsers native momentum scroll and is thereby not suitable for most applications. With some refinement however (only prevent if at top scrolling up or at bottom scrolling down, ...) this method fixes most problems. Many possible implementations can be found in this SO post.
Type 2:
Overflow scrolling on the body however is not prevented by methods described above.
One possible solution which seems reasonable is to prevent the element from ever being at its top or bottom position as described as best solution on mentioned question.
anElement.addEventListener('touchstart', function( event ){
if( this.scrollTop === 0 ) {
this.scrollTop += 1;
} else if( this.scrollTop + this.offsetHeight >= this.scrollHeight ) {
this.scrollTop -= 1;
}
}
This however did not reliably work on iOS 9.3.2.
What did work however is setting position: fixed
on the <body>
element to prevent the body from moving. Please note however that this still does not completely stop type 2
from happening, which is why sometimes you cannot scroll/focus any element because in the background type2
with its focus lock is still happening (again, after you stop touching the screen for a moment it again works as expected).
While this is still far from being the optimal solution it seems to be the best we can get for the time speaking.
Edit: Please note that I am not sure if it is safe to put position: fixed
on a <body>
element. To track possible issues I have created following SO post. Apparently it might be better to create a wrapper element as child of body and set that element to position: fixed
to avoid zoom problemes.
Edit 2: The definite solution
The script iNoBounce works wonders. Just load it to the page and experience a bounce-free web application. So far I have not found any problems with this solution.
-webkit-overflow-scrolling: touch; not working on iPhone 6+ in Safari
IOS 8 does not read -webkit-overflow-scrolling: touch so you should also insert overflow:scroll to fix this problem on IOS 8.
in this case the versions which before 8 will read "-webkit-overflow-scrolling: touch" and the 8th version will read "overflow:scroll".
I faced the same problem and I fixed it in this way.
Related Topics
How to Schedule Mail Using Gmail API
Can't Write Non Current User Objects by Pfuser Currentuser
Core Data Nstimeinterval Using an Accessor Directly Is Buggy
iOS - Combine/Concatenate Multiple Audio Files
iOS How to Get a List of Already Purchased Products
How to Request Photo Gallery Permissions in iOS a Second Time
Swift - Weekday by Current Location by Currentcalendar()
Conversion from String to Date in Swift Returns Nil
Why [Uiscreen Mainscreen].Bounds] Is Not Returning Full Screen Size
Filter Array of Custom Objects in Swift
Realm Crashes with Rlmexception: Object Has Been Deleted or Invalidated
Calculate The Range of Visible Text in UIlabel
Sprite Frame Animation Cocos2D 3.0
Removing Parentheses from The String in iOS
Is There a Way of Automatically Writing Custom Values to The Bundle's .Plist During a Build Phase