Reputation: 19158
I'm trying to disable the html/body scrollbar of the parent while I'm using a lightbox. The main word here is disable. I do not want to hide it with overflow: hidden;
.
The reason for this is that overflow: hidden
makes the site jump and take up the area where the scroll was.
I want to know if its possible to disable a scrollbar while still showing it.
Upvotes: 276
Views: 289762
Reputation: 6025
We can solve this now using css scrollbar-gutter.
https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-gutter
Whenever you disable an element using Javascript just add the js-disabled-scroll
class, and then add global css as follows.
.js-disabled-scroll {
overflow: hidden;
scrollbar-gutter: stable;
}
no available on IOs yet so a workaround using old techniques:
.io-only .js-disabled-scroll {
overflow: hidden;
padding-right: var(--wathever-default-scroll-with);
}
Upvotes: 1
Reputation: 1807
Idk why nobody points this out, just catch the wheel
or scroll
event.
When you want to disable scrolling;
yourDIV.addEventListener('wheel', scrollLock)
When you want to enable scrolling again
yourDIV.removeEventListener('wheel', scrollLock)
While calling preventDefault
on your event
function scrollLock(event: WheelEvent) {
// prevent scrolling
event.preventDefault()
event.stopPropagation()
}
To use this in plain Javascript, remove the type WheelEvent
.
To prevent all types of scrolling, e.g. when pressing the arrow keys capture the scroll
event.
Upvotes: -1
Reputation: 123397
If the page under the overlayer can be "fixed" at the top, when you open the overlay you can set
body {
position: fixed;
overflow-y: scroll;
}
you should still see the right scrollbar but the content is not scrollable. When you close the overlay just revert these properties with
body {
position: static;
overflow-y: auto;
}
I just proposed this way only because you wouldn't need to change any scroll event
What if I already scrolled the page?
if you get the document.documentElement.scrollTop
property via javascript just before the layer opening, you could dynamically assign that value as top
property of the body
element: with this approach the page will keep its current scroll position, no matter if you're on top or if you have already scrolled.
Css
.noscroll {
position: fixed;
top: var(--st, 0);
inline-size: 100%;
overflow-y:scroll;
}
JS
const b = document.body;
b.style.setProperty('--st', -(document.documentElement.scrollTop) + "px");
b.classList.add('noscroll');
Upvotes: 238
Reputation: 11
I have created a repository to solve this exact issue. Below, I have boiled the solution down to its essentials, but the repository with horizontal scrolling support and other customization options can be found here: https://github.com/alexspirgel/no-scroll
Here is a link straight to the working demo page: http://alexanderspirgel.com/no-scroll/demo/
My solution works by comparing the difference in dimensions between an outer element with a scrollbar and its inner element. Once the scrollbar size is calculated, the scrolling element can be set to overflow: hidden
and an offset equal to the scrollbar width can be applied to prevent content shift. Additionally, this solution places a pseudo element with a disabled scrollbar in the new space created after disabling scrolling.
First, setup the html so there is an inner element wrapped by an outer element (the outer and inner elements can be the <html>
and <body>
elements).
<html>
<body>
content here...
</body>
</html>
The script sets CSS variables and applies the correct HTML data attributes, but the rest is controlled by the CSS. Include this CSS so the elements have the necessary styles.
[data-no-scroll-element=outer] {
--no-scroll--y-scrollbar-width: 0px;
box-sizing: border-box;
position: relative;
padding: 0; /* Padding on outer elements breaks disabled scrollbar placeholder spacing. */
overflow: auto; /* Accounts for space of internal margins. */
}
[data-no-scroll-element=outer][data-no-scroll-y-state=no-scroll] {
overflow-y: hidden;
}
[data-no-scroll-element=outer]::after {
content: "";
box-sizing: border-box;
display: none;
position: sticky;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 100%;
height: 100%;
overflow-x: hidden;
overflow-y: hidden;
pointer-events: none;
}
[data-no-scroll-element=outer][data-no-scroll-y-state=no-scroll]::after {
display: block;
overflow-y: scroll;
}
html[data-no-scroll-element=outer]::after {
height: 100vh; /* If the outer element is <html> the after element must be set to the viewport height to work properly. */
}
[data-no-scroll-element=inner] {
box-sizing: border-box;
display: block;
width: auto;
height: auto;
max-width: none;
max-height: none;
border: 0; /* Border on inner elements breaks scrollbar size calculations. */
margin: 0; /* Margin on inner elements breaks scrollbar size calculations. */
padding: 0; /* Padding on inner elements breaks scrollbar size calculations. */
overflow: auto; /* Accounts for space of internal margins. */
}
[data-no-scroll-element=outer][data-no-scroll-y-state=no-scroll] > [data-no-scroll-element=inner] {
margin-right: var(--no-scroll--y-scrollbar-width, 0px);
}
Include this JavaScript to handle the calculation of the scrollbar width and handle the switching between enabling and disabling scrolling.
class noScroll {
static isOuterElementDocumentElement(outerElement) {
if (outerElement === document.documentElement) {
return true;
}
else {
return false;
}
}
static getElementYScrollbarWidth(outerElement, innerElement) {
let size = 0;
let outerSize = outerElement.offsetWidth;
if (this.isOuterElementDocumentElement(outerElement)) {
outerSize = window.innerWidth;
}
const innerSize = innerElement.offsetWidth;
const outerElementComputedStyles = window.getComputedStyle(outerElement);
const borderLeftWidth = parseInt(outerElementComputedStyles.borderLeftWidth);
const borderRightWidth = parseInt(outerElementComputedStyles.borderRightWidth);
size = (outerSize - borderLeftWidth - borderRightWidth) - innerSize;
if (size < 0) {
size = 0;
}
return size;
}
static setYScrollbarWidthCSSVariable(outerElement, innerElement, value) {
if (typeof value === 'undefined') {
value = this.getElementYScrollbarWidth(outerElement, innerElement);
}
if (typeof value === 'number') {
value = value.toString() + 'px';
}
outerElement.style.setProperty('--no-scroll--y-scrollbar-width', value);
if (this.isOuterElementDocumentElement(outerElement)) {
outerElement.style.setProperty('--no-scroll--document-width-offset', value);
}
}
static isScrollEnabled(outerElement) {
if (outerElement.getAttribute('data-no-scroll-y-state') === 'no-scroll') {
return false;
}
return true;
}
static disableScroll(outerElement, innerElement) {
outerElement.setAttribute('data-no-scroll-element', 'outer');
innerElement.setAttribute('data-no-scroll-element', 'inner');
this.setYScrollbarWidthCSSVariable(outerElement, innerElement);
outerElement.setAttribute('data-no-scroll-y-state', 'no-scroll');
}
static enableScroll(outerElement, innerElement) {
this.setYScrollbarWidthCSSVariable(outerElement, innerElement, 0);
outerElement.setAttribute('data-no-scroll-y-state', 'scroll');
}
static toggleScroll(outerElement, innerElement) {
const isScrollEnabled = this.isScrollEnabled(outerElement);
if (isScrollEnabled) {
this.disableScroll(outerElement, innerElement);
}
else {
this.enableScroll(outerElement, innerElement);
}
}
};
Then the scroll can be disabled/enabled as you see fit by calling the methods like this:
const outerElement = document.querySelector('html');
const innerElement = document.querySelector('body');
// disable
noScroll.disableScroll(outerElement, innerElement);
// enable
noScroll.enableScroll(outerElement, innerElement);
// toggle
noScroll.toggleScroll(outerElement, innerElement);
If you are disabling scroll on the <html>
element, the width of the website will change. This will cause position: fixed
elements to have the undesirable "jump" in position. To solve this, when disabling scroll of the <html>
element, a CSS variable called --no-scroll--document-width-offset
is created with a value equal to the change in width. You can include this CSS variable in the position of fixed elements to prevent the "jump" like so:
.fixed-centered {
position: fixed;
left: calc((100% - var(--no-scroll--document-width-offset, 0px)) / 2);
transform: translateX(-50%);
}
I hope this works for you. Again this is a boiled down version of my more complete solution located here: https://github.com/alexspirgel/no-scroll
Upvotes: -1
Reputation: 179
JavaScript:
const disableScroll = () => {
window.scrollTo({
top: 0,
behavior: "instant"
})
}
document.addEventListener("scroll", disableScroll, false)
React + TS:
type Props = {
open: boolean
onClose: () => void
disableClickOutside?: boolean
disabledEscKey?: boolean
width?: string
theme?: Theme
size?: Size
children?: ReactNode
}
export const Modal = ({
open,
children,
width,
theme = "light",
onClose,
disableClickOutside,
disabledEscKey,
size = "md",
}: Props) => {
const contentRef: RefObject<HTMLDivElement> = useRef<HTMLDivElement>(null)
useEffect(() => {
const { scrollTop } = document.documentElement
const disableScroll = () => {
window.scrollTo({
top: scrollTop,
behavior: "instant" as ScrollBehavior,
})
}
if (open) {
contentRef?.current?.focus()
document.addEventListener("scroll", disableScroll, false)
} else if (!open) {
document.removeEventListener("scroll", disableScroll)
}
return () => {
document.removeEventListener("scroll", disableScroll)
}
}, [open])
const handleClickOutside = (event: React.MouseEvent<Element>) => {
if (disableClickOutside) return
const clickedElement = event.target as HTMLDivElement
if (!contentRef?.current?.contains(clickedElement) && open) onClose()
}
const handleKeydown = (event: React.KeyboardEvent<Element>) => {
if (disabledEscKey) return
if (event.key === "Escape") onClose()
}
return (
<Wrapper
aria-label="modal-wrapper"
isOpen={open}
onKeyDown={(e: React.KeyboardEvent<Element>) => handleKeydown(e)}
onClick={(e: React.MouseEvent<Element>) => handleClickOutside(e)}
tabIndex={-1}
>
<Content
theme={theme}
ref={contentRef}
width={width}
size={size}
tabIndex={1}
>
{children}
</Content>
</Wrapper>
)
}
type StyledModalProps = {
width?: string
theme?: Theme
size: Size
}
const Wrapper = styled.div`
display: ${(props: { isOpen: boolean }) => (props.isOpen ? "flex" : "none")};
align-items: center;
justify-content: center;
z-index: 3;
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.3);
&:focus {
outline: none;
}
`
const Content = styled.div`
display: flex;
justify-content: center;
background-color: ${({ theme }: StyledModalProps) =>
match(theme)
.with("dark", () => Gray90)
.otherwise(() => White)};
padding: 2rem;
border-radius: 1rem;
box-shadow: 0px 0px 1rem
${({ theme }: StyledModalProps) =>
match(theme)
.with("dark", () => "rgba(0, 0, 0, 0.48)")
.otherwise(() => "rgba(28, 28, 31, 0.12)")};
width: ${({ width, size }: StyledModalProps) =>
width ||
match(size)
.with("lg", () => "67.5rem")
.with("md", () => "45rem")
.with("sm", () => "28.75rem")
.with("xs", () => "20rem")
.exhaustive()};
`
Upvotes: 0
Reputation: 5169
React version:
import type {
PropsWithChildren
} from "react";
import {
useCallback,
useState,
useContext,
createContext
} from "react";
type BlanketContextShape = {
isOpen ? : boolean;
zIndex: number;
color: string;
setIsOpen: (yesno ? : boolean) => void;
};
const BlanketContext = createContext<BlanketContextShape>({
zIndex: 500,
color: `rgba(0,0,0,0.5)`,
setIsOpen() {
return;
},
});
function useBlanket() {
const context = useContext(BlanketContext);
if (!context)
throw new Error(
"useBlanket can only be used within children of BlanketProvider"
);
return context;
}
function BlanketProvider({
children,
color,
zIndex,
}: PropsWithChildren<{
zIndex ? : number;
color ? : string;
}>) {
const [isOpen, setIsOpen] = useState<boolean>(false);
const [top, setTop] = useState(0);
const handleSetIsOpen = useCallback(
(yesno?: boolean) => {
if (typeof window === "undefined") return;
if (yesno) {
const scrollTop = window.scrollY;
document.body.style.top = `-${scrollTop}px`;
setTop(scrollTop);
}
if (window.innerHeight < document.body.scrollHeight) {
document.body.style.overflowY = (!!yesno && "scroll") || "auto";
document.body.style.position = (!!yesno && "fixed") || "static";
}
window.scrollTo({ top });
setIsOpen(() => !!yesno);
}, [top]
);
return (
<BlanketContext.Provider
value={{
isOpen,
setIsOpen: handleSetIsOpen,
color: color || `rgba(0,0,0,0.5)`,
zIndex: zIndex || 200
}}>
{children}
</BlanketContext.Provider>
);
}
function Blanket({
children
}: PropsWithChildren) {
const {
isOpen,
setIsOpen,
zIndex,
color
} = useBlanket();
return (
<>
{isOpen && (
<div
style={{
position: "fixed",
backgroundColor: color,
top: 0,
height: "100vh",
width: "100vw",
zIndex: zIndex,
}}
onClick = {() => setIsOpen(false)}
/>
)}
{children}
</>
);
}
https://gist.github.com/airtonix/c8c9af146185646e7451faa0f2ac96b7
use it like:
// app
<BlanketProvider color='red'>
<YourView />
</BlanketProvider>
// YourView
...
const { isOpen, setIsOpen } = useBlanket();
return (
<>
<Blanket>
{isOpen && <SomeThingWithHigherZindex />}
</Blanket>
<Button onClick={() => setIsOpen(true)}>Do A Thing</Button>
</>
)
When setting isOpen
to true, we track the current scroll from top and store it.
We only set it on the body styles if we're opening, because what we do next will cause window.scrollY
to be 0
.
Then we test if the document is taller than the viewport.
If it is then we set the Y overflow to scroll
to ensure that the scrollbar doesn't vanish and that there's no layout reflow jump.
Set body to position fixed, preventing it from scrolling and ensure that the document is at the correct scrolling position in order to counteract position: fixed;
Upvotes: 0
Reputation: 51
I solved this problem with a scrollLock method that set up listeners for scroll wheel events and key down events, and a preventScroll method that handled the events. Something like this:
preventScroll = function (e) {
// prevent scrollwheel events
e.preventDefault();
e.stopPropagation();
// prevent keydown events
var keys = [32, 33, 34, 35, 37, 38, 39, 40];
if (keys.includes(e.keyCode)) {
e.preventDefault();
}
return false;
}
scrollLock = function (lock) {
if (lock) {
document.querySelector("#container").addEventListener("wheel", preventScroll);
document.addEventListener("keydown", preventScroll);
}
else {
document.querySelector("#container").removeEventListener("wheel", preventScroll);
document.querySelector("#container").removeEventListener("keydown", preventScroll);
}
}
Upvotes: -1
Reputation: 2390
I have some other fixed elements in the page and setting body
's position
to fixed
caused a bunch of other problems, so I did it in a hacky way:
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
// on opening modal
document.body.style.overflow = "hidden"
document.body.style.paddingRight = `${scrollbarWidth}px`
// on closing modal
document.body.style.overflow = "unset",
document.body.style.paddingRight = "0px"
The idea is to add a padding-right
with the same width as browser's scrollbar, to mimick a fake scrollbar and prevent the content shift.
Upvotes: 1
Reputation: 157
Here is a working demo. This is how you can do this with pure JavaScript:
const { body, documentElement } = document;
let { scrollTop } = document.documentElement;
function disableScroll() {
scrollTop = documentElement.scrollTop;
body.style.top = `-${scrollTop}px`;
body.classList.add("scroll-disabled");
}
function enableScroll() {
body.classList.remove("scroll-disabled");
documentElement.scrollTop = scrollTop;
body.style.removeProperty("top");
}
And this is the CSS:
.scroll-disabled {
position: fixed;
width: 100%;
overflow-y: scroll;
}
We use position: fixed
on body
to prevent it from being scrollable and we use overflow-y
to show the scrollbar. We also need to set width
because of how position: fixed
works.
We keep track of the scroll position and update it when disabling scroll so that we can position body
appropriately using top
when scroll is disabled and restore the scroll position when it is enabled. Otherwise body
will keep jumping to the top when disabling or enabling scroll.
When enabling scroll we remove the top
style from body
. This prevents it from breaking your layout if you have a different position
than static
on body
.
If you are using scroll-behavior: smooth
on html
, you also need to modify the enableScroll
function like this:
function enableScroll() {
body.classList.remove("scroll-disabled");
// Set "scroll-behavior" to "auto"
documentElement.style.scrollBehavior = "auto";
documentElement.scrollTop = scrollTop;
// Remove "scroll-behavior: auto" after restoring scroll position
documentElement.style.removeProperty("scroll-behavior");
body.style.removeProperty("top");
}
We need to temporarily set scroll-behavior
to auto
so that there are no jumps.
Upvotes: 7
Reputation: 298
I’ve noticed that the YouTube website does exactly this. So by inspecting it a bit I’ve been able to determine that they’re using @polymer/iron-overlay-behavior
and fortunately, it can be used rather unobtrusively outside of web components/Polymer:
import {
pushScrollLock,
removeScrollLock,
} from '@polymer/iron-overlay-behavior/iron-scroll-manager';
// lock scroll everywhere except scrollElement
pushScrollLock(scrollElement);
// restore scrolling
removeScrollLock(scrollElement);
It seems like a mature solution and surely the best I was able to find. The package is a bit heavy but I guess much of it become unbundled, when importing just the iron-scroll-manager
.
Cheers
Upvotes: 1
Reputation: 2122
Four little additions to the accepted solution:
Complete solution that seems to work for most browsers:
CSS
html.noscroll {
position: fixed;
overflow-y: scroll;
width: 100%;
}
Disable scroll
if ($(document).height() > $(window).height()) {
var scrollTop = ($('html').scrollTop()) ? $('html').scrollTop() : $('body').scrollTop(); // Works for Chrome, Firefox, IE...
$('html').addClass('noscroll').css('top',-scrollTop);
}
Enable scroll
var scrollTop = parseInt($('html').css('top'));
$('html').removeClass('noscroll');
$('html,body').scrollTop(-scrollTop);
Thanks to Fabrizio and Dejan for putting me on the right track and to Brodingo for the solution to the double scroll bar
Upvotes: 109
Reputation: 79
I have made this one function, that solves this problem with JS. This principle can be easily extended and customized that is a big pro for me.
Using this js DOM API function:
const handleWheelScroll = (element) => (event) => {
if (!element) {
throw Error("Element for scroll was not found");
}
const { deltaY } = event;
const { clientHeight, scrollTop, scrollHeight } = element;
if (deltaY < 0) {
if (-deltaY > scrollTop) {
element.scrollBy({
top: -scrollTop,
behavior: "smooth",
});
event.stopPropagation();
event.preventDefault();
}
return;
}
if (deltaY > scrollHeight - clientHeight - scrollTop) {
element.scrollBy({
top: scrollHeight - clientHeight - scrollTop,
behavior: "smooth",
});
event.stopPropagation();
event.preventDefault();
return;
}
};
In short, this function will stop event propagation and default behavior if the scroll would scroll something else then the given element (the one you want to scroll in).
Then you can hook and unhook this up like this:
const wheelEventHandler = handleWheelScroll(elementToScrollIn);
window.addEventListener("wheel", wheelEventHandler, {
passive: false,
});
window.removeEventListener("wheel", wheelEventHandler);
Watch out for that it is a higher order function so you have to keep a reference to the given instance.
I hook the addEventListener
part in mouse enter and unhook the removeEventListener
in mouse leave events in jQuery, but you can use it as you like.
Upvotes: 1
Reputation: 6284
The position: fixed;
solution has a drawback - the page jumps to the top when this style is applied. Angular's Material Dialog has a nice solution, where they fake the scroll position by applying positioning to the html
element.
Below is my revised algorithm for vertical scrolling only. Left scroll blocking is done in the exact same manner.
// This class applies the following styles:
// position: fixed;
// overflow-y: scroll;
// width: 100%;
const NO_SCROLL_CLASS = "bp-no-scroll";
const coerceCssPixelValue = value => {
if (value == null) {
return "";
}
return typeof value === "string" ? value : `${value}px`;
};
export const blockScroll = () => {
const html = document.documentElement;
const documentRect = html.getBoundingClientRect();
const { body } = document;
// Cache the current scroll position to be restored later.
const cachedScrollPosition =
-documentRect.top || body.scrollTop || window.scrollY || document.scrollTop || 0;
// Cache the current inline `top` value in case the user has set it.
const cachedHTMLTop = html.style.top || "";
// Using `html` instead of `body`, because `body` may have a user agent margin,
// whereas `html` is guaranteed not to have one.
html.style.top = coerceCssPixelValue(-cachedScrollPosition);
// Set the magic class.
html.classList.add(NO_SCROLL_CLASS);
// Return a function to remove the scroll block.
return () => {
const htmlStyle = html.style;
const bodyStyle = body.style;
// We will need to seamlessly restore the original scroll position using
// `window.scroll`. To do that we will change the scroll behavior to `auto`.
// Here we cache the current scroll behavior to restore it later.
const previousHtmlScrollBehavior = htmlStyle.scrollBehavior || "";
const previousBodyScrollBehavior = bodyStyle.scrollBehavior || "";
// Restore the original inline `top` value.
htmlStyle.top = cachedHTMLTop;
// Remove the magic class.
html.classList.remove(NO_SCROLL_CLASS);
// Disable user-defined smooth scrolling temporarily while we restore the scroll position.
htmlStyle.scrollBehavior = bodyStyle.scrollBehavior = "auto";
// Restore the original scroll position.
window.scroll({
top: cachedScrollPosition.top
});
// Restore the original scroll behavior.
htmlStyle.scrollBehavior = previousHtmlScrollBehavior;
bodyStyle.scrollBehavior = previousBodyScrollBehavior;
};
};
The logic is very simple and can be simplified even more if you don't care about certain edge cases. For example, this is what I use:
export const blockScroll = () => {
const html = document.documentElement;
const documentRect = html.getBoundingClientRect();
const { body } = document;
const screenHeight = window.innerHeight;
// Only do the magic if document is scrollable
if (documentRect.height > screenHeight) {
const cachedScrollPosition =
-documentRect.top || body.scrollTop || window.scrollY || document.scrollTop || 0;
html.style.top = coerceCssPixelValue(-cachedScrollPosition);
html.classList.add(NO_SCROLL_CLASS);
return () => {
html.classList.remove(NO_SCROLL_CLASS);
window.scroll({
top: cachedScrollPosition,
behavior: "auto"
});
};
}
};
Upvotes: 1
Reputation: 2131
Another solution to get rid of content jump on fixed modal, when removing body scroll is to normalize page width:
body {width: 100vw; overflow-x: hidden;}
Then you can play with fixed position or overflow:hidden for body when the modal is open. But it will hide horizontal scrollbars - usually they're not needed on responsive website.
Upvotes: 3
Reputation: 5422
This will stop the viewport jumping to the top by saving the scroll position and restoring it on enabling scrolling.
CSS
.no-scroll{
position: fixed;
width:100%;
min-height:100vh;
top:0;
left:0;
overflow-y:scroll!important;
}
JS
var scrollTopPostion = 0;
function scroll_pause(){
scrollTopPostion = $(window).scrollTop();
$("body").addClass("no-scroll").css({"top":-1*scrollTopPostion+"px"});
}
function scroll_resume(){
$("body").removeClass("no-scroll").removeAttr("style");
$(window).scrollTop(scrollTopPostion);
}
Now all you need to do is to call the functions
$(document).on("click","#DISABLEelementID",function(){
scroll_pause();
});
$(document).on("click","#ENABLEelementID",function(){
scroll_resume();
});
Upvotes: 1
Reputation: 283223
You can hide the body's scrollbar with overflow: hidden
and set a margin at the same time so that the content doesn't jump:
let marginRightPx = 0;
if(window.getComputedStyle) {
let bodyStyle = window.getComputedStyle(document.body);
if(bodyStyle) {
marginRightPx = parseInt(bodyStyle.marginRight, 10);
}
}
let scrollbarWidthPx = window.innerWidth - document.body.clientWidth;
Object.assign(document.body.style, {
overflow: 'hidden',
marginRight: `${marginRightPx + scrollbarWidthPx}px`
});
And then you can add a disabled scrollbar to the page to fill in the gap:
textarea {
overflow-y: scroll;
overflow-x: hidden;
width: 11px;
outline: none;
resize: none;
position: fixed;
top: 0;
right: 0;
bottom: 0;
border: 0;
}
<textarea></textarea>
I did exactly this for my own lightbox implementation. Seems to be working well so far.
Upvotes: 6
Reputation: 19158
I'm the OP
With the help of answer from fcalderan I was able to form a solution. I leave my solution here as it brings clarity to how to use it, and adds a very crucial detail, width: 100%;
I add this class
body.noscroll
{
position: fixed;
overflow-y: scroll;
width: 100%;
}
this worked for me and I was using Fancyapp.
Upvotes: 24
Reputation: 154
If the page under the overlayer can be "fixed" at the top, when you open the overlay you can set
.disableScroll { position: fixed; overflow-y:scroll }
provide this class to the scrollable body, you should still see the right scrollbar but the content is not scrollable.
To maintain the position of the page do this in jquery
$('body').css('top', - ($(window).scrollTop()) + 'px').addClass('disableScroll');
When you close the overlay just revert these properties with
var top = $('body').position().top;
$('body').removeClass('disableScroll').css('top', 0).scrollTop(Math.abs(top));
I just proposed this way only because you wouldn't need to change any scroll event
Upvotes: 1
Reputation: 11
All modal/lightbox javascript-based systems use an overflow when displaying the modal/lightbox, on html tag or body tag.
When lightbox is show, the js push a overflow hidden on html or body tag. When lightbox is hidden, some remove the hidden other push a overflow auto on html or body tag.
Developers who work on Mac, do not see the problem of the scrollbar.
Just replace the hidden by an unset not to see the content slipping under the modal of the removal of the scrollbar.
Lightbox open/show:
<html style="overflow: unset;"></html>
Lightbox close/hide:
<html style="overflow: auto;"></html>
Upvotes: 1
Reputation: 139
This worked really well for me....
// disable scrolling
$('body').bind('mousewheel touchmove', lockScroll);
// enable scrolling
$('body').unbind('mousewheel touchmove', lockScroll);
// lock window scrolling
function lockScroll(e) {
e.preventDefault();
}
just wrap those two lines of code with whatever decides when you are going to lock scrolling.
e.g.
$('button').on('click', function() {
$('body').bind('mousewheel touchmove', lockScroll);
});
Upvotes: 13
Reputation: 657
I had a similar problem: a left-hand menu that, when it appears, prevents scrolling. As soon as height was set to 100vh, the scrollbar disappeared and the content jerked to the right.
So if you don't mind keeping the scrollbar enabled (but setting the window to full height so it won't actually scroll anywhere) then another possibility is setting a tiny bottom margin, which will keep the scroll bars showing:
body {
height: 100vh;
overflow: hidden;
margin: 0 0 1px;
}
Upvotes: 1
Reputation: 5494
You cannot disable the scroll event, but you can disable the related actions that lead to a scroll, like mousewheel and touchmove:
$('body').on('mousewheel touchmove', function(e) {
e.preventDefault();
});
Upvotes: 8
Reputation: 697
$.fn.disableScroll = function() {
window.oldScrollPos = $(window).scrollTop();
$(window).on('scroll.scrolldisabler',function ( event ) {
$(window).scrollTop( window.oldScrollPos );
event.preventDefault();
});
};
$.fn.enableScroll = function() {
$(window).off('scroll.scrolldisabler');
};
//disable
$("#selector").disableScroll();
//enable
$("#selector").enableScroll();
Upvotes: 39
Reputation: 11
<div id="lightbox">
is inside the <body>
element, thus when you scroll the lightbox you also scroll the body. The solution is to not extend the <body>
element over 100%, to place the long content inside another div
element and to add a scrollbar if needed to this div
element with overflow: auto
.
html {
height: 100%
}
body {
margin: 0;
height: 100%
}
#content {
height: 100%;
overflow: auto;
}
#lightbox {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
<html>
<body>
<div id="content">much content</div>
<div id="lightbox">lightbox<div>
</body>
</html>
Now, scrolling over the lightbox (and the body
as well) has no effect, because the body is no longer than 100% of the screen height.
Upvotes: 1
Reputation: 28756
I like to stick to the "overflow: hidden" method and just add padding-right that's equal to the scrollbar width.
Get scrollbar width function, by lostsource.
function getScrollbarWidth() {
var outer = document.createElement("div");
outer.style.visibility = "hidden";
outer.style.width = "100px";
outer.style.msOverflowStyle = "scrollbar"; // needed for WinJS apps
document.body.appendChild(outer);
var widthNoScroll = outer.offsetWidth;
// force scrollbars
outer.style.overflow = "scroll";
// add innerdiv
var inner = document.createElement("div");
inner.style.width = "100%";
outer.appendChild(inner);
var widthWithScroll = inner.offsetWidth;
// remove divs
outer.parentNode.removeChild(outer);
return widthNoScroll - widthWithScroll;
}
When showing the overlay, add "noscroll" class to html and add padding-right to body:
$(html).addClass("noscroll");
$(body).css("paddingRight", getScrollbarWidth() + "px");
When hiding, remove the class and padding:
$(html).removeClass("noscroll");
$(body).css("paddingRight", 0);
The noscroll style is just this:
.noscroll { overflow: hidden; }
Note that if you have any elements with position:fixed you need to add the padding to those elements too.
Upvotes: 2
Reputation: 18795
This is the solution we went with. Simply save the scroll position when the overlay is opened, scroll back to the saved position any time the user attempted to scroll the page, and turn the listener off when the overlay is closed.
It's a bit jumpy on IE, but works like a charm on Firefox/Chrome.
var body = $("body"),
overlay = $("#overlay"),
overlayShown = false,
overlayScrollListener = null,
overlaySavedScrollTop = 0,
overlaySavedScrollLeft = 0;
function showOverlay() {
overlayShown = true;
// Show overlay
overlay.addClass("overlay-shown");
// Save scroll position
overlaySavedScrollTop = body.scrollTop();
overlaySavedScrollLeft = body.scrollLeft();
// Listen for scroll event
overlayScrollListener = body.scroll(function() {
// Scroll back to saved position
body.scrollTop(overlaySavedScrollTop);
body.scrollLeft(overlaySavedScrollLeft);
});
}
function hideOverlay() {
overlayShown = false;
// Hide overlay
overlay.removeClass("overlay-shown");
// Turn scroll listener off
if (overlayScrollListener) {
overlayScrollListener.off();
overlayScrollListener = null;
}
}
// Click toggles overlay
$(window).click(function() {
if (!overlayShown) {
showOverlay();
} else {
hideOverlay();
}
});
/* Required */
html, body { margin: 0; padding: 0; height: 100%; background: #fff; }
html { overflow: hidden; }
body { overflow-y: scroll; }
/* Just for looks */
.spacer { height: 300%; background: orange; background: linear-gradient(#ff0, #f0f); }
.overlay { position: fixed; top: 20px; bottom: 20px; left: 20px; right: 20px; z-index: -1; background: #fff; box-shadow: 0 0 5px rgba(0, 0, 0, .3); overflow: auto; }
.overlay .spacer { background: linear-gradient(#88f, #0ff); }
.overlay-shown { z-index: 1; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<h1>Top of page</h1>
<p>Click to toggle overlay. (This is only scrollable when overlay is <em>not</em> open.)</p>
<div class="spacer"></div>
<h1>Bottom of page</h1>
<div id="overlay" class="overlay">
<h1>Top of overlay</h1>
<p>Click to toggle overlay. (Containing page is no longer scrollable, but this is.)</p>
<div class="spacer"></div>
<h1>Bottom of overlay</h1>
</div>
Upvotes: 3
Reputation: 66388
Crude but working way will be to force the scroll back to top, thus effectively disabling scrolling:
var _stopScroll = false;
window.onload = function(event) {
document.onscroll = function(ev) {
if (_stopScroll) {
document.body.scrollTop = "0px";
}
}
};
When you open the lightbox raise the flag and when closing it,lower the flag.
Upvotes: 0
Reputation: 2382
you can keep overflow:hidden but manage scroll position manually:
before showing keep trace of actual scroll position:
var scroll = [$(document).scrollTop(),$(document).scrollLeft()];
//show your lightbox and then reapply scroll position
$(document).scrollTop(scroll[0]).scrollLeft(scroll[1]);
it should work
Upvotes: 1
Reputation: 15375
You can do it with Javascript:
// Classic JS
window.onscroll = function(ev) {
ev.preventDefault();
}
// jQuery
$(window).scroll(function(ev) {
ev.preventDefault();
}
And then disable it when your lightbox is closed.
But if your lightbox contains a scroll bar, you won't be able to scroll while it's open. This is because window
contains both body
and #lightbox
.
So you have to use an architecture like the following one:
<body>
<div id="global"></div>
<div id="lightbox"></div>
</body>
And then apply the onscroll
event only on #global
.
Upvotes: -1