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.
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;
inline-size: 100%;
overflow-y:scroll
}
JS
$('body').css('top', -(document.documentElement.scrollTop) + 'px')
.addClass('noscroll');
Four little additions to the accepted solution:
Apply 'noscroll' to html instead of to body to prevent double scroll bars in IE
To check if there's actually a scroll bar before adding the 'noscroll' class. Otherwise, the site will also jump pushed by the new non-scrolling scroll bar.
To keep any possible scrollTop so the entire page doesn't go back to the top (like Fabrizio's update, but you need to grab the value before adding the 'noscroll' class)
Not all browsers handle scrollTop the same way as documented at http://help.dottoro.com/ljnvjiow.php
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
With jQuery inluded:
disable
$.fn.disableScroll = function() {
window.oldScrollPos = $(window).scrollTop();
$(window).on('scroll.scrolldisabler',function ( event ) {
$(window).scrollTop( window.oldScrollPos );
event.preventDefault();
});
};
enable
$.fn.enableScroll = function() {
$(window).off('scroll.scrolldisabler');
};
usage
//disable
$("#selector").disableScroll();
//enable
$("#selector").enableScroll();
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.
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);
});
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();
});
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.
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.
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>
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.
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.
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
<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.
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;
}
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>
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
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();
});
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"
});
};
}
};
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.
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);
Allows scrolling in selected element
Doesn't mess with styling in any way
Is battle-tested on YouTube website
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
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.
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.
Live test case.
React version:
ππ» keeps scroll position
ππ» no layout reflow
ππ» typescript
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;
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.
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);
}
}
Related
I'm using the new position: sticky (info) to create an iOS-like list of content.
It's working well and far superior than the previous JavaScript alternative (example) however as far as I know no event is fired when it's triggered, which means I can't do anything when the bar hits the top of the page, unlike with the previous solution.
I'd like to add a class (e.g. stuck) when an element with position: sticky hits the top of the page. Is there a way to listen for this with JavaScript? Usage of jQuery is fine.
Demo with IntersectionObserver (use a trick):
// get the sticky element
const stickyElm = document.querySelector('header')
const observer = new IntersectionObserver(
([e]) => e.target.classList.toggle('isSticky', e.intersectionRatio < 1),
{threshold: [1]}
);
observer.observe(stickyElm)
body{ height: 200vh; font:20px Arial; }
section{
background: lightblue;
padding: 2em 1em;
}
header{
position: sticky;
top: -1px; /* β the trick */
padding: 1em;
padding-top: calc(1em + 1px); /* β compensate for the trick */
background: salmon;
transition: .1s;
}
/* styles for when the header is in sticky mode */
header.isSticky{
font-size: .8em;
opacity: .5;
}
<section>Space</section>
<header>Sticky Header</header>
The top value needs to be -1px or the element will never intersect with the top of the browser window (thus never triggering the intersection observer).
To counter this 1px of hidden content, an additional 1px of space should be added to either the border or the padding of the sticky element.
π‘ Alternatively, if you wish to keep the CSS as is (top:0), then you can apply the "correction" at the intersection observer-level by adding the setting rootMargin: '-1px 0px 0px 0px' (as #mattrick showed in his answer)
Demo with old-fashioned scroll event listener:
auto-detecting first scrollable parent
Throttling the scroll event
Functional composition for concerns-separation
Event callback caching: scrollCallback (to be able to unbind if needed)
// get the sticky element
const stickyElm = document.querySelector('header');
// get the first parent element which is scrollable
const stickyElmScrollableParent = getScrollParent(stickyElm);
// save the original offsetTop. when this changes, it means stickiness has begun.
stickyElm._originalOffsetTop = stickyElm.offsetTop;
// compare previous scrollTop to current one
const detectStickiness = (elm, cb) => () => cb & cb(elm.offsetTop != elm._originalOffsetTop)
// Act if sticky or not
const onSticky = isSticky => {
console.clear()
console.log(isSticky)
stickyElm.classList.toggle('isSticky', isSticky)
}
// bind a scroll event listener on the scrollable parent (whatever it is)
// in this exmaple I am throttling the "scroll" event for performance reasons.
// I also use functional composition to diffrentiate between the detection function and
// the function which acts uppon the detected information (stickiness)
const scrollCallback = throttle(detectStickiness(stickyElm, onSticky), 100)
stickyElmScrollableParent.addEventListener('scroll', scrollCallback)
// OPTIONAL CODE BELOW ///////////////////
// find-first-scrollable-parent
// Credit: https://stackoverflow.com/a/42543908/104380
function getScrollParent(element, includeHidden) {
var style = getComputedStyle(element),
excludeStaticParent = style.position === "absolute",
overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/;
if (style.position !== "fixed")
for (var parent = element; (parent = parent.parentElement); ){
style = getComputedStyle(parent);
if (excludeStaticParent && style.position === "static")
continue;
if (overflowRegex.test(style.overflow + style.overflowY + style.overflowX))
return parent;
}
return window
}
// Throttle
// Credit: https://jsfiddle.net/jonathansampson/m7G64
function throttle (callback, limit) {
var wait = false; // Initially, we're not waiting
return function () { // We return a throttled function
if (!wait) { // If we're not waiting
callback.call(); // Execute users function
wait = true; // Prevent future invocations
setTimeout(function () { // After a period of time
wait = false; // And allow future invocations
}, limit);
}
}
}
header{
position: sticky;
top: 0;
/* not important styles */
background: salmon;
padding: 1em;
transition: .1s;
}
header.isSticky{
/* styles for when the header is in sticky mode */
font-size: .8em;
opacity: .5;
}
/* not important styles*/
body{ height: 200vh; font:20px Arial; }
section{
background: lightblue;
padding: 2em 1em;
}
<section>Space</section>
<header>Sticky Header</header>
Here's a React component demo which uses the first technique
I found a solution somewhat similar to #vsync's answer, but it doesn't require the "hack" that you need to add to your stylesheets. You can simply change the boundaries of the IntersectionObserver to avoid needing to move the element itself outside of the viewport:
const observer = new IntersectionObserver(callback, {
rootMargin: '-1px 0px 0px 0px',
threshold: [1],
});
observer.observe(element);
If anyone gets here via Google one of their own engineers has a solution using IntersectionObserver, custom events, and sentinels:
https://developers.google.com/web/updates/2017/09/sticky-headers
Just use vanilla JS for it. You can use throttle function from lodash to prevent some performance issues as well.
const element = document.getElementById("element-id");
document.addEventListener(
"scroll",
_.throttle(e => {
element.classList.toggle(
"is-sticky",
element.offsetTop <= window.scrollY
);
}, 500)
);
After Chrome added position: sticky, it was found to be not ready enough and relegated to to --enable-experimental-webkit-features flag. Paul Irish said in February "feature is in a weird limbo state atm".
I was using the polyfill until it become too much of a headache. It works nicely when it does, but there are corner cases, like CORS problems, and it slows page loads by doing XHR requests for all your CSS links and reparsing them for the "position: sticky" declaration that the browser ignored.
Now I'm using ScrollToFixed, which I like better than StickyJS because it doesn't mess up my layout with a wrapper.
There is currently no native solution. See Targeting position:sticky elements that are currently in a 'stuck' state. However I have a CoffeeScript solution that works with both native position: sticky and with polyfills that implement the sticky behavior.
Add 'sticky' class to elements you want to be sticky:
.sticky {
position: -webkit-sticky;
position: -moz-sticky;
position: -ms-sticky;
position: -o-sticky;
position: sticky;
top: 0px;
z-index: 1;
}
CoffeeScript to monitor 'sticky' element positions and add the 'stuck' class when they are in the 'sticky' state:
$ -> new StickyMonitor
class StickyMonitor
SCROLL_ACTION_DELAY: 50
constructor: ->
$(window).scroll #scroll_handler if $('.sticky').length > 0
scroll_handler: =>
#scroll_timer ||= setTimeout(#scroll_handler_throttled, #SCROLL_ACTION_DELAY)
scroll_handler_throttled: =>
#scroll_timer = null
#toggle_stuck_state_for_sticky_elements()
toggle_stuck_state_for_sticky_elements: =>
$('.sticky').each ->
$(this).toggleClass('stuck', this.getBoundingClientRect().top - parseInt($(this).css('top')) <= 1)
NOTE: This code only works for vertical sticky position.
I came up with this solution that works like a charm and is pretty small. :)
No extra elements needed.
It does run on the window scroll event though which is a small downside.
apply_stickies()
window.addEventListener('scroll', function() {
apply_stickies()
})
function apply_stickies() {
var _$stickies = [].slice.call(document.querySelectorAll('.sticky'))
_$stickies.forEach(function(_$sticky) {
if (CSS.supports && CSS.supports('position', 'sticky')) {
apply_sticky_class(_$sticky)
}
})
}
function apply_sticky_class(_$sticky) {
var currentOffset = _$sticky.getBoundingClientRect().top
var stickyOffset = parseInt(getComputedStyle(_$sticky).top.replace('px', ''))
var isStuck = currentOffset <= stickyOffset
_$sticky.classList.toggle('js-is-sticky', isStuck)
}
Note: This solution doesn't take elements that have bottom stickiness into account. This only works for things like a sticky header. It can probably be adapted to take bottom stickiness into account though.
I know it has been some time since the question was asked, but I found a good solution to this. The plugin stickybits uses position: sticky where supported, and applies a class to the element when it is 'stuck'. I've used it recently with good results, and, at time of writing, it is active development (which is a plus for me) :)
I'm using this snippet in my theme to add .is-stuck class to .site-header when it is in a stuck position:
// noinspection JSUnusedLocalSymbols
(function (document, window, undefined) {
let windowScroll;
/**
*
* #param element {HTMLElement|Window|Document}
* #param event {string}
* #param listener {function}
* #returns {HTMLElement|Window|Document}
*/
function addListener(element, event, listener) {
if (element.addEventListener) {
element.addEventListener(event, listener);
} else {
// noinspection JSUnresolvedVariable
if (element.attachEvent) {
element.attachEvent('on' + event, listener);
} else {
console.log('Failed to attach event.');
}
}
return element;
}
/**
* Checks if the element is in a sticky position.
*
* #param element {HTMLElement}
* #returns {boolean}
*/
function isSticky(element) {
if ('sticky' !== getComputedStyle(element).position) {
return false;
}
return (1 >= (element.getBoundingClientRect().top - parseInt(getComputedStyle(element).top)));
}
/**
* Toggles is-stuck class if the element is in sticky position.
*
* #param element {HTMLElement}
* #returns {HTMLElement}
*/
function toggleSticky(element) {
if (isSticky(element)) {
element.classList.add('is-stuck');
} else {
element.classList.remove('is-stuck');
}
return element;
}
/**
* Toggles stuck state for sticky header.
*/
function toggleStickyHeader() {
toggleSticky(document.querySelector('.site-header'));
}
/**
* Listen to window scroll.
*/
addListener(window, 'scroll', function () {
clearTimeout(windowScroll);
windowScroll = setTimeout(toggleStickyHeader, 50);
});
/**
* Check if the header is not stuck already.
*/
toggleStickyHeader();
})(document, window);
#vsync 's excellent answer was almost what I needed, except I "uglify" my code via Grunt, and Grunt requires some older JavaScript code styles. Here is the adjusted script I used instead:
var stickyElm = document.getElementById('header');
var observer = new IntersectionObserver(function (_ref) {
var e = _ref[0];
return e.target.classList.toggle('isSticky', e.intersectionRatio < 1);
}, {
threshold: [1]
});
observer.observe( stickyElm );
The CSS from that answer is unchanged
Something like this also works for a fixed scroll height:
// select the header
const header = document.querySelector('header');
// add an event listener for scrolling
window.addEventListener('scroll', () => {
// add the 'stuck' class
if (window.scrollY >= 80) navbar.classList.add('stuck');
// remove the 'stuck' class
else navbar.classList.remove('stuck');
});
As you can see here : http://www.shadownet.com.mv/products-2/
The first container sticks on top when scrolled vertically from the container. It should end and the second container "SHADOW SERVER SERIES" should stick on top replacing the first one (bringing the first one to the original position) when it is scrolled vertically from the container.
Right now i use this JS code to make the first one stick but when i use it for the second one, it sticks both on top and doesn't give the intended results :
var menu = document.querySelector('#sticky')
var menuPosition = menu.getBoundingClientRect().top;
window.addEventListener('scroll', function() {
if (window.pageYOffset >= menuPosition) {
menu.style.position = 'fixed';
menu.style.top = '42px';
menu.style.width = '100%';
} else {
menu.style.position = 'static';
menu.style.top = '';
}
});
I apologize for my bad english, im not a native speaker.
If you can use CSS, I would use the menuPosition as state in CSS.
So first the JS function would control state:
var menu = document.querySelector('#sticky'),
menuPosition = menu.getBoundingClientRect().top;
window.addEventListener('scroll', function() {
if (window.pageYOffset >= menuPosition){
document.body.classList.add('scrolled');
} else {
document.body.classList.remove('scrolled');
}
});
I've used classList which has a polyfill.
As Obsidian Age pointed out, move the variable menuPosition inside the event controller when it's CSS is dynamic from the top.
Then in CSS, use the body state to control offset for both containers:
#sticky { display: none; position: static; top: 48px; /*...*/ }
#sticky + #sticky2 { display: none; position: static; width: 100%;/*...*/ }
.scrolled #sticky { display: block; position: fixed; }
.scrolled #sticky + #sticky2 { display: block; position: fixed; }
The + in CSS only works if both containers are direct children of the same parent.
For this particular one,
I used stickyJS which worked wonderfully and out of the box. I was using a wordpress website. I added the script to header.php and added the JS snippet on desired page, set IDs for the two containers and gave z-index so that they go above each other when in view.
I have a script where I once I reach a specific point of the screen the div starts to scroll with the body by adding a fixed position, but it is not smooth and looks horrible. I have not been able to understand if I can add a pageoffset instead of adding a fixed class which causes the div to slightly jump instead of just picking up from where it is.
window.onload = function ()
{
var scrolledElement = document.getElementById('ID');
var top = scrolledElement.offsetTop;
var listener = function ()
{
var y = scrolledElement.scrollTop || scrolledElement.scrollTop || window.pageYOffset;
if (y >= top)
{
scrolledElement.classList.add('fixed');
} else {
scrolledElement.classList.remove('fixed');
}
};
window.addEventListener('scroll', listener, false);
}
but as I said, whenever the body scroll reaches the ID div it just jumps and adds the fixed css which is..
#ID.fixed {
position: fixed;
top:0;
width: 336px;
margin: 0px auto;
margin-top: 15px;
}
I can't understand if by adding offsettop to it instead of css it would make it not jump?
Let's say I have a single HTML page. 2000 pixels long for example. I want to detect if a visitor reaches a certain point on the page.
The page structure:
0px = begin of the page;
500px = about us page;
1000px = contactpage;
Is there a way with jQuery to detect if a user reaches the points described above?
You probably want jQuery's scroll event-binding function.
Yes, I would create three divs and then have a mouse over event on each. Example:
$("#begin").mouseover(function(){
alert("over begin");
});
$("#about").mouseover(function(){
alert("over about");
});
$("#contact").mouseover(function(){
alert("over contact");
});
You can see a working fiddle here: http://jsfiddle.net/ezj9F/
Try THIS working snippet.
Using this code you don't have to know position of the element you want to check if it is visible.
JQuery
var $window = $(window);
// # of pixels from the top of the document to the top of div.content
var contentTop = $("div.content").offset().top;
// content is visible when it is on the bottom of the window and not at the top
var contentStart = contentTop - $window.height();
// content is still visible if any part of his height is visible
var contentEnd = contentTop + $("div.content").height();
$window.scroll(function() {
var scrollTop = $window.scrollTop();
if(scrollTop > contentStart && scrollTop < contentEnd) {
console.log('You can see "HELLO"!');
} else {
console.log('You cannot see "HELLO"!');
}
});
HTML
<div class="scroll"></div>
<div class="content">HELLO</div>
<div class="scroll"></div>
CSS
div.scroll {
background-color: #eee;
width: 100px;
height: 1000px;
}
div.content {
background-color: #bada55;
width: 100px;
height: 200px;
}
EDIT: Now the algorithm is checking if any part of the div.content is visible (it is considering height of the element). If you are not interested in that change contentEnd to var contentEnd = contentTop.
I need that when I scroll down or up with mouse wheel or sidebar my div change incrementally the Y position (for example 50px up or down ). I need this in Javascript/Jquery.
I Try this code, but only works for scrolling down(The Scrolling Down and Up Function is working well, only the animate part is working wrong):
UPDATE:
var sidebarScrollTop = 0;
$(document).ready(function() {
sidebarScrollTop = $("body").offset();
$(window).scroll(function ()
{
var docScrollTop = $('body,html').scrollTop();
if(docScrollTop > sidebarScrollTop.top)
{
$("#legend").stop().animate({ marginTop: "+=50px",}, 'slow', "easeOutCirc" );
}
else
{
$("#legend").stop().animate({ marginTop: "-=50px",}, 'slow', "easeOutCirc" );
}
});
});
$(window).resize(function()
{
sidebarScrollTop = $("#legend").offset().top;
});
$(document).resize(function()
{
sidebarScrollTop = $("#legend").offset().top;
});
Thanks
You can use
$(window).scroll(function() {
// Your scroll code here
});
to grab whenever the user is scrolling on the page.
Next you want to change the div's y-value.
If the div is positioned absolute, this is just changing its top-value.
$('my-div').top = original-top-value + $(window).pageYOffset;
I believe you need is to keep the div always showing even when user scrolls down. If that is the case then it can be done with only CSS:
div {
position: fixed;
z-index: 100;
top: 100px;
left: 100px;
}
The values of z-index, top and left are dummy values. Change em with your ones.
UPDATE:
Since CSS Solution won't work for you, here is a working example writter in JS: http://jsfiddle.net/qCtt5/