Home > Net >  Jquery animated smooth vertical scroll only for specified sections specified by a custom attribute
Jquery animated smooth vertical scroll only for specified sections specified by a custom attribute

Time:01-04

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>

  • Related