I am facing a challenge where before scroll, you will notice when scrolling down or up, there is a slight pixel shift in the same direction before a smooth scroll animation occurs and snaps to the next or previous section depending on the scroll direction.
Code can be found here: https://stackblitz.com/edit/html-sample-rpcn3b?file=index.html
Preview can be found here: https://html-sample-rpcn3b.stackblitz.io
How can I remove this slight shift before the animated scroll happens, so that I can get a smooth scroll snapping from one section to the next. In the end (for the targeted sections) I want to achieve a smooth scroll closely similar to this example here with swiperjs: https://swiper-master.webflow.io/full-page-vertical
CodePudding user response:
Your code seems to work as expected if you tweak a bit wtd
and st
in getClosestElement
(see my HERE
comments in the snippet below).
(function ($) {
$.fn.sectionsnap = function (options) {
var settings = {
delay: 50, // time dilay (ms)
selector: '[fs-animated-section]', // selector
reference: 1, // % of window height from which we start
animationTime: 300, // animation time (snap scrolling)
offsetTop: 0, // offset top (no snap before scroll reaches this position)
offsetBottom: 0, // offset bottom (no snap after bottom - offsetBottom)
}
var $wrapper = this,
direction = 'down',
currentScrollTop = $(window).scrollTop(),
scrollTimer,
animating = false;
// check the direction
var updateDirection = function () {
if ($(window).scrollTop() >= currentScrollTop) direction = 'down';
else direction = 'up';
currentScrollTop = $(window).scrollTop();
};
// return the closest element (depending on direction)
var getClosestElement = function () {
var $list = $wrapper.find(settings.selector),
wt = $(window).scrollTop(),
wh = $(window).height(),
refY = wh * settings.reference,
wtd = wt refY - 3, // <--- HERE
$target;
if (direction == 'down') {
$list.each(function () {
var st = $(this).position().top - 1; // <--- HERE
if (st > wt && st <= wtd) {
$target = $(this);
return false; // just to break the each loop
}
});
} else {
wtd = wt - refY 3; // <--- HERE
$list.each(function () {
var st = $(this).position().top - 1; // <--- HERE
if (st < wt && st >= wtd) {
$target = $(this);
return false; // just to break the each loop
}
});
}
return $target;
};
// snap
var snap = function () {
var $target = getClosestElement();
if ($target) {
animating = true;
$('html, body').animate(
{
scrollTop: $target.offset().top,
},
settings.animationTime,
function () {
window.clearTimeout(scrollTimer);
animating = false;
}
);
}
};
// on window scroll
var windowScroll = function () {
if (animating) return;
var st = $(window).scrollTop();
if (st < settings.offsetTop) return;
if (st > $('html').height() - $(window).height() - settings.offsetBottom)
return;
updateDirection();
window.clearTimeout(scrollTimer);
scrollTimer = window.setTimeout(snap, settings.delay);
};
$(window).scroll(windowScroll);
return this;
};
})(jQuery);
$(document).ready(function () {
$('[fs-animated-container="container"]').sectionsnap();
});
section {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
border-bottom: 10px solid blue;
}
<!DOCTYPE html>
<html>
<head>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev nYRRuWlolflfl" crossorigin="anonymous" />
</head>
<body>
<div id="app">
<div >
<div fs-animated-container="container">
<section fs-animated-section="first" >
<div >
<div >
<h1> Section ONE ANIMATED SCROLL
</h1>
</div>
</div>
</section>
<section fs-animated-section="second" >
<div >
<div >
<h1> Section TWO ANIMATED SCROLL
</h1>
</div>
</div>
</section>
<section fs-animated-section="third" >
<div >
<div >
<h1> Section THREE ANIMATED SCROLL
</h1>
</div>
</div>
</section>
</div>
<section >
<div >
<div >
<h1> Section FOUR NORMAL SCROLL
</h1>
</div>
</div>
</section>
<section >
<div >
<div >
<h1> Section FIVE NORMAL SCROLL
</h1>
</div>
</div>
</section>
<section >
<div >
<div >
<h1> Section SIX NORMAL SCROLL
</h1>
</div>
</div>
</section>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.3/jquery.min.js"
integrity="sha512-STof4xm1wgkfm7heWqFJVn58Hm3EtS31XFaagaa8VMReCXAkQnJZ jEy8PCC/iT18dFy95WcExNHFTqLyp72eQ=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
</body>
</html>
Moreover, some of your HTML tags are not closed. See <div >
.
EDIT 1
After reading your comment, it seems that I misunderstood the meaning of your question...
In fact, except if your challenge is to reinvent the wheel, you may want to use a small library like jQuery Easing Plugin. You will find below the same example with a custom easing function and no delay
:
(function ($) {
$.fn.sectionsnap = function (options) {
var settings = {
delay: 0, // <--- HERE
selector: '[fs-animated-section]', // selector
reference: 1, // % of window height from which we start
animationTime: 300, // animation time (snap scrolling)
offsetTop: 0, // offset top (no snap before scroll reaches this position)
offsetBottom: 0, // offset bottom (no snap after bottom - offsetBottom)
}
var $wrapper = this,
direction = 'down',
currentScrollTop = $(window).scrollTop(),
scrollTimer,
animating = false;
// check the direction
var updateDirection = function () {
if ($(window).scrollTop() >= currentScrollTop) direction = 'down';
else direction = 'up';
currentScrollTop = $(window).scrollTop();
};
// return the closest element (depending on direction)
var getClosestElement = function () {
var $list = $wrapper.find(settings.selector),
wt = $(window).scrollTop(),
wh = $(window).height(),
refY = wh * settings.reference,
wtd = wt refY - 3,
$target;
if (direction == 'down') {
$list.each(function () {
var st = $(this).position().top - 1;
if (st > wt && st <= wtd) {
$target = $(this);
return false; // just to break the each loop
}
});
} else {
wtd = wt - refY 3;
$list.each(function () {
var st = $(this).position().top - 1;
if (st < wt && st >= wtd) {
$target = $(this);
return false; // just to break the each loop
}
});
}
return $target;
};
// snap
var snap = function () {
var $target = getClosestElement();
if ($target) {
animating = true;
$('html, body').animate(
{
scrollTop: $target.offset().top,
},
settings.animationTime,
'easeOutElastic', // <--- HERE
function () {
window.clearTimeout(scrollTimer);
animating = false;
}
);
}
};
// on window scroll
var windowScroll = function () {
if (animating) return;
var st = $(window).scrollTop();
if (st < settings.offsetTop) return;
if (st > $('html').height() - $(window).height() - settings.offsetBottom)
return;
updateDirection();
window.clearTimeout(scrollTimer);
scrollTimer = window.setTimeout(snap, settings.delay);
};
$(window).scroll(windowScroll);
return this;
};
})(jQuery);
$(document).ready(function () {
$('[fs-animated-container="container"]').sectionsnap();
});
section {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
border-bottom: 10px solid blue;
}
<!DOCTYPE html>
<html>
<head>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev nYRRuWlolflfl" crossorigin="anonymous" />
</head>
<body>
<div id="app">
<div >
<div fs-animated-container="container">
<section fs-animated-section="first" >
<div >
<div >
<h1> Section ONE ANIMATED SCROLL
</h1>
</div>
</div>
</section>
<section fs-animated-section="second" >
<div >
<div >
<h1> Section TWO ANIMATED SCROLL
</h1>
</div>
</div>
</section>
<section fs-animated-section="third" >
<div >
<div >
<h1> Section THREE ANIMATED SCROLL
</h1>
</div>
</div>
</section>
</div>
<section >
<div >
<div >
<h1> Section FOUR NORMAL SCROLL
</h1>
</div>
</div>
</section>
<section >
<div >
<div >
<h1> Section FIVE NORMAL SCROLL
</h1>
</div>
</div>
</section>
<section >
<div >
<div >
<h1> Section SIX NORMAL SCROLL
</h1>
</div>
</div>
</section>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.3/jquery.min.js"
integrity="sha512-STof4xm1wgkfm7heWqFJVn58Hm3EtS31XFaagaa8VMReCXAkQnJZ jEy8PCC/iT18dFy95WcExNHFTqLyp72eQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-easing/1.4.1/jquery.easing.min.js" integrity="sha512-0QbL0ph8Tc8g5bLhfVzSqxe9GERORsKhIn1IrpxDAgUsbBGz/V7iSav2zzW325XGd1OMLdL4UiqRJj702IeqnQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
</body>
</html>
It looks much smoother now!
For the record, you might also want to use CSS instead of JS:
div[fs-animated-container] {
scroll-snap-type: y mandatory;
overflow-y: scroll;
height: 100vh;
}
section[fs-animated-section] {
scroll-snap-align: center;
}
section {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
border-bottom: 10px solid blue;
}
<!DOCTYPE html>
<html>
<head>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev nYRRuWlolflfl" crossorigin="anonymous" />
</head>
<body>
<div id="app">
<div >
<div fs-animated-container="container">
<section fs-animated-section="first" >
<div >
<div >
<h1> Section ONE ANIMATED SCROLL
</h1>
</div>
</div>
</section>
<section fs-animated-section="second" >
<div >
<div >
<h1> Section TWO ANIMATED SCROLL
</h1>
</div>
</div>
</section>
<section fs-animated-section="third" >
<div >
<div >
<h1> Section THREE ANIMATED SCROLL
</h1>
</div>
</div>
</section>
</div>
<section >
<div >
<div >
<h1> Section FOUR NORMAL SCROLL
</h1>
</div>
</div>
</section>
<section >
<div >
<div >
<h1> Section FIVE NORMAL SCROLL
</h1>
</div>
</div>
</section>
<section >
<div >
<div >
<h1> Section SIX NORMAL SCROLL
</h1>
</div>
</div>
</section>
</div>
</div>
</body>
</html>
It does not work very well on IE, though. This browser is just too old.
EDIT 2
If you want something even smoother, I am not sure you need .animate()
. I removed it from the code. Of course, the jQuery Easing Plugin is not necessary anymore.
Besides, even though it is not compulsory, I recommend you to implement throttling to increase performance (the scroll event fires way too often for what we need to do). The easiest strategy here is to use _.throttle()
from Lodash.
(function ($) {
$.fn.sectionsnap = function (options) {
var settings = {
selector: '[fs-animated-section]', // selector
reference: 1, // % of window height from which we start
offsetTop: 0, // offset top (no snap before scroll reaches this position)
offsetBottom: 0 // offset bottom (no snap after bottom - offsetBottom)
}
var $wrapper = this,
direction = 'down',
currentScrollTop = $(window).scrollTop();
// check the direction
var updateDirection = function () {
if ($(window).scrollTop() >= currentScrollTop) direction = 'down';
else direction = 'up';
currentScrollTop = $(window).scrollTop();
};
// return the closest element (depending on direction)
var getClosestElement = function () {
var $list = $wrapper.find(settings.selector),
wt = $(window).scrollTop(),
wh = $(window).height(),
refY = wh * settings.reference,
wtd = wt refY - 3,
$target;
if (direction == 'down') {
$list.each(function () {
var st = $(this).position().top - 1;
if (st > wt && st <= wtd) {
$target = $(this);
return false; // just to break the each loop
}
});
} else {
wtd = wt - refY 3;
$list.each(function () {
var st = $(this).position().top - 1;
if (st < wt && st >= wtd) {
$target = $(this);
return false; // just to break the each loop
}
});
}
return $target;
};
// snap
var snap = function () {
var $target = getClosestElement();
if ($target) {
$('html, body').scrollTop($target.offset().top); // <--- HERE
}
};
// on window scroll
var windowScroll = function () {
var st = $(window).scrollTop();
if (st < settings.offsetTop) return;
if (st > $('html').height() - $(window).height() - settings.offsetBottom)
return;
updateDirection();
snap();
};
$(window).on('scroll', _.throttle(windowScroll, 50, { trailing: false })); // <--- HERE
return this;
};
})(jQuery);
$(document).ready(function () {
$('[fs-animated-container="container"]').sectionsnap();
});
section {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
border-bottom: 10px solid blue;
}
<!DOCTYPE html>
<html>
<head>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev nYRRuWlolflfl" crossorigin="anonymous" />
</head>
<body>
<div id="app">
<div >
<div fs-animated-container="container">
<section fs-animated-section="first" >
<div >
<div >
<h1> Section ONE ANIMATED SCROLL
</h1>
</div>
</div>
</section>
<section fs-animated-section="second" >
<div >
<div >
<h1> Section TWO ANIMATED SCROLL
</h1>
</div>
</div>
</section>
<section fs-animated-section="third" >
<div >
<div >
<h1> Section THREE ANIMATED SCROLL
</h1>
</div>
</div>
</section>
</div>
<section >
<div >
<div >
<h1> Section FOUR NORMAL SCROLL
</h1>
</div>
</div>
</section>
<section >
<div >
<div >
<h1> Section FIVE NORMAL SCROLL
</h1>
</div>
</div>
</section>
<section >
<div >
<div >
<h1> Section SIX NORMAL SCROLL
</h1>
</div>
</div>
</section>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.3/jquery.min.js"
integrity="sha512-STof4xm1wgkfm7heWqFJVn58Hm3EtS31XFaagaa8VMReCXAkQnJZ jEy8PCC/iT18dFy95WcExNHFTqLyp72eQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js" integrity="sha512-WFN04846sdKMIP5LKNphMaWzU7YpMyCU245etK3g/2ARYbPK9Ub18eG ljU96qKRCWh quCY7yefSmlkQw1ANQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
</body>
</html>