What is the right way to smoothly pin an element according to scroll position? I tried debouncing a scroll listener for performance but the pinning is not accurate. Even with debouncing set to 10ms it's not smooth and the element doesn't snap cleanly to its initial position.
var scrolling = false;
var stickPosY = 100;
var heights = [];
$(".element").each( function(index) {
heights[index] = $(".element[data-trigger=" + index + "]").offset().top;
});
function pin() {
if ( !$("#aside").hasClass("fixed") ) {
var stickyLeft = $("#aside").offset().left;
var stickyWidth = $("#aside").outerWidth();
var stickyTop = $("#aside").offset().top - stickPosY;
$("#aside").addClass("fixed");
$("#aside").css({"left": stickyLeft, "top": stickyTop, "width": stickyWidth});
}
}
function unpin() {
$("#aside").css({"left": "", "top": "", "width": ""});
$("#aside").removeClass("fixed")
}
$( window ).scroll( function() {
scrolling = true;
});
setInterval( function() {
if ( scrolling ) {
scrolling = false;
var y = window.scrollY;
console.log(y);
// PIN SIDEBAR
y > stickPosY ? pin() : unpin();
//TRIGGERS
for (var i=0; i < heights.length; i++) {
if (y >= heights[i]) {
$('.element[data-trigger="' + i + '"]').addClass("blue");
}
else {
$('.element[data-trigger="' + i + '"]').removeClass("blue");
}
}
}
}, 250 );
Here's my Pen
I tried to use scrollMagic for the project on a scene with a pin and additional triggers but the scrolling wasn't very smooth. So I'm trying to rebuild it with a stripped-down version and debounced listeners. Is this approach possible, or should I rather try to optimize my scrollMagic scene?
As James points out, you can just use position: sticky as one option, but that doesn't work in older browsers and its uses are limited to simpler situations in newer browsers, so I'll continue with the JS solution assuming you want to go that route.
There is a lot going on in your JS, and I think you are probably overcomplicating things, so I will give you a few basics to consider.
When you are toggling things based on scroll, either toggle inline styles or a class, but not both. I would recommend toggling a class because it allows you to have one function that can work on multiple screen sizes (i.e., you can use media queries to change the behavior of your toggled class based on screen size). Also it keeps all your styles in one place instead of having them split between your JS and your stylesheet.
Try to keep the work you're doing while scrolling as minimal as possible. For example, cache references to elements in variables outside your scroll function so you're not continually looking them up every time you scroll a pixel. Avoid loops inside scroll functions.
Using setInterval is not generally the recommended approach for increasing performance on scroll functions. All that is going to do is run a function every X amount of time, all the time, whether you're scrolling or not. What you really want to do is rate-limit your scroll function directly. That way, if you scroll a long ways real fast your function will only be called a fraction of the total times it would otherwise be called, but if you scroll a short distance slowly it will still be called a minimum number of times to keep things looking smooth, and if you don't scroll at all then you're not calling your function at all. Also, you probably want to throttle your function in this case, not debounce it.
Consider using the throttle function from Underscore.js or Lodash.js instead of inventing your own because those ones are highly performant and guaranteed to work across a wide variety of browsers.
Here is a simple example of sticking an element to the top of the screen on scroll, throttled with Lodash. I'm using a 25ms throttle, which is about the maximum amount I'd recommend for keeping things looking smooth where you won't really notice the delay in the element sticking/unsticking as you scroll past your threshold. You could go down to as little as 10ms.
$(function() {
$(window).on('scroll', _.throttle(toggleClass, 25));
const myThing = $('#my-thing');
const threshold = $('#dummy-1').height();
function toggleClass() {
const y = window.scrollY;
if (y > threshold) {
myThing.addClass('stuck')
} else {
myThing.removeClass('stuck');
}
}
});
#dummy-1 {
height: 150px;
background-color: steelblue;
}
#dummy-2 {
height: 150px;
background-color: gold;
}
#my-thing {
width: 300px;
height: 75px;
background-color: firebrick;
position: absolute;
top: 150px;
left: 0;
}
#my-thing.stuck {
position: fixed;
top: 0;
}
body {
margin: 0;
height: 2000px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.0.0/lodash.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="dummy-1"></div>
<div id="dummy-2"></div>
<div id="my-thing"></div>