skip to Main Content

I’m trying to implement a smooth scroll with scrollTo. I cannot control the speed if I use the option scrollIntoView({ behavior: 'smooth' }. Hence I use the combination of performance.now() and requestAnimationFrame to achieve the effect.

It works well on mousewheel event, the problem is that I have three buttons, it should go to the relative section as I click. However it doesn’t work for the button.

The mouse wheel event is using the window.scrollTo(0, startY + distance * progress); while the button use the window.scrollTo(0, manualPosition * progress); where the manualPosition is the section position by lis[index].getBoundingClientRect().top.

Please try to run the code and click the button, or use mouse wheel to see the effect.

(function () {
    const lis = document.querySelectorAll('.section'),
        btnsNav = document.querySelectorAll('.button'),
        speed = 500;
    let currentSection = 0,
        isScrolling = false;

    function smoothScrollTo(time, direction, mode = 'auto', index = 1) {
        if (isScrolling) {
            return;
        }

        isScrolling = true;

        const start = performance.now(),
            startY = window.scrollY,
            distance =
                direction === 'up' ? -window.innerHeight : window.innerHeight;

        function animateScroll() {
            const now = performance.now(),
                elapsed = now - start,
                progress = elapsed / time,
                manualPosition = lis[index].getBoundingClientRect().top;

            if (mode === 'auto') {
                window.scrollTo(0, startY + distance * progress);
            } else if (mode === 'manual') {
                window.scrollTo(0, manualPosition * progress);
            }

            if (elapsed < time) {
                requestAnimationFrame(animateScroll);
            } else {
                isScrolling = false;
            }
        }

        requestAnimationFrame(animateScroll);

        if (direction === 'down') {
            currentSection++;
        } else if (direction === 'up') {
            currentSection--;
        }
    }

    document.addEventListener('wheel', function (event) {
        if (event.deltaY > 0 && currentSection < lis.length - 1) {
            smoothScrollTo(speed, 'down');
        } else if (event.deltaY < 0 && currentSection > 0) {
            smoothScrollTo(speed, 'up');
        }
    });

    for (let index = 0; index < btnsNav.length; index++) {
        btnsNav[index].addEventListener('click', function () {
            smoothScrollTo(speed, 'down', 'manual', index);
        });
    }
})();
body {
    margin: 0;
}

#div {
    position: fixed;
    top: 0;
    left: 0;
    display: flex;
    column-gap: 10px;
}

.section {
    height: 100vh;
    width: 100%;
}

.section:nth-of-type(1) {
    background-color: red;
}

.section:nth-of-type(2) {
    background-color: yellow;
}

.section:nth-of-type(3) {
    background-color: green;
}
<div id="div">
  <button class="button" type="button">Go to 1</button>
  <button class="button" type="button">Go to 2</button>
  <button class="button" type="button">Go to 3</button>
</div>

<section class="section"></section>
<section class="section"></section>
<section class="section"></section>

2

Answers


  1. Issues in the code

    I found two problems in your code,

    1. The btnsNav variable: You have been storing .section in this variable and , attaching the event listener there, which is incorrect. Change it to .button

      btnsNav = document.querySelectorAll('.button'),
      
    2. When you are calling the smoothScrollTo function in the for loop, you are passing manual as the mode. Passing the mode as auto seems to resolve the issue.

      smoothScrollTo(speed, 'down', 'auto', index);
      
    Note: There are still some more changes required in the logic which I will leave you to fix as I have been able to go to the required page downwards but the button click does not take us upward to the page. Cheers!

    Hints: The direction you are passing on the smoothScrollTo in the for loop is always down. Instead of using ‘up’/’down’ I would use currentSection and the index being passed to identify whether to scroll up or down. Simplify the logic 😉

    Code with fixes:

    (function () {
        const lis = document.querySelectorAll('.button'),
            btnsNav = document.querySelectorAll('.button'),
            speed = 500;
        let currentSection = 0,
            isScrolling = false;
    
        function smoothScrollTo(time, direction, mode = 'auto', index = 1) {
            if (isScrolling) {
                return;
            }
    
            isScrolling = true;
    
            const start = performance.now(),
                startY = window.scrollY,
                distance =
                    direction === 'up' ? -window.innerHeight : window.innerHeight;
    
            function animateScroll() {
                const now = performance.now(),
                    elapsed = now - start,
                    progress = elapsed / time,
                    manualPosition = lis[index].getBoundingClientRect().top;
    
                if (mode === 'auto') {
                    window.scrollTo(0, startY + distance * progress);
                } else if (mode === 'manual') {
                    window.scrollTo(0, manualPosition * progress);
                }
    
                if (elapsed < time) {
                    requestAnimationFrame(animateScroll);
                } else {
                    isScrolling = false;
                }
            }
    
            requestAnimationFrame(animateScroll);
    
            if (direction === 'down') {
                currentSection++;
            } else if (direction === 'up') {
                currentSection--;
            }
        }
    
        document.addEventListener('wheel', function (event) {
            if (event.deltaY > 0 && currentSection < lis.length - 1) {
                smoothScrollTo(speed, 'down');
            } else if (event.deltaY < 0 && currentSection > 0) {
                smoothScrollTo(speed, 'up');
            }
        });
    
        for (let index = 0; index < btnsNav.length; index++) {
            btnsNav[index].addEventListener('click', function () {
                smoothScrollTo(speed, 'down', 'auto', index);
            });
        }
    })();
    body {
        margin: 0;
    }
    
    #div {
        position: fixed;
        top: 0;
        left: 0;
        display: flex;
        column-gap: 10px;
    }
    
    .section {
        height: 100vh;
        width: 100%;
    }
    
    .section:nth-of-type(1) {
        background-color: red;
    }
    
    .section:nth-of-type(2) {
        background-color: yellow;
    }
    
    .section:nth-of-type(3) {
        background-color: green;
    }
    <div id="div">
      <button class="button" type="button">Go to 1</button>
      <button class="button" type="button">Go to 2</button>
      <button class="button" type="button">Go to 3</button>
    </div>
    
    <section class="section"></section>
    <section class="section"></section>
    <section class="section"></section>
    Login or Signup to reply.
  2. You can actually achieve this effect without any javascript at all – it’s possible with HTML + CSS.

    There are three steps:

    1. Apply scroll-behavior: smooth to the document root (ie. the <html> element):
    :root {
      scroll-behavior: smooth;
    }
    
    1. Surround each button with a hashfragment link:
    <a href="#section2"><button class="button" type="button">Go to 2</button></a>
    
    1. Add a corresponding id to each .section to indicate the hashfragment link destinations:
    <section id="section2" class="section"></section>
    

    Working Example:

    :root {
      scroll-behavior: smooth;
    }
    
    body {
        margin: 0;
    }
    
    #div {
        position: fixed;
        top: 0;
        left: 0;
        display: flex;
        column-gap: 10px;
    }
    
    .section {
        height: 100vh;
        width: 100%;
    }
    
    .section:nth-of-type(1) {
        background-color: red;
    }
    
    .section:nth-of-type(2) {
        background-color: yellow;
    }
    
    .section:nth-of-type(3) {
        background-color: green;
    }
    <div id="div">
      <a href="#section1"><button class="button" type="button">Go to 1</button></a>
      <a href="#section2"><button class="button" type="button">Go to 2</button></a>
      <a href="#section3"><button class="button" type="button">Go to 3</button></a>
    </div>
    
    <section id="section1" class="section"></section>
    <section id="section2" class="section"></section>
    <section id="section3" class="section"></section>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search