Home >Web Front-end >JS Tutorial >How to Implement Smooth Scrolling in Vanilla JavaScript

How to Implement Smooth Scrolling in Vanilla JavaScript

William Shakespeare
William ShakespeareOriginal
2025-02-18 10:49:09507browse

How to Implement Smooth Scrolling in Vanilla JavaScript

Core points

  • Use the Jump.js library to implement native JavaScript smooth scrolling, simplifying scrolling animation without external dependencies.
  • Modify Jump.js original code to convert it from ES6 to ES5 to ensure wider compatibility with different browsers.
  • Use the requestAnimationFrame method to perform smooth animation updates, optimize performance and provide a smoother user experience.
  • Implement custom JavaScript to intercept the default in-page link behavior, and replace sudden jumps with smooth scrolling animations.
  • Integrated CSS scroll-behavior Properties to support native smooth scrolling in browsers that recognize this feature, and JavaScript fallback mechanism is provided if the browser does not support it.
  • Ensure accessibility by setting focus to target elements after scrolling, addressing potential issues with keyboard navigation, and enhancing usability for all users.

This article was peer-reviewed by Adrian Sandu, Chris Perry, Jérémy Heleine and Mallory van Achterberg. Thanks to all the peer reviewers of SitePoint to get the best content in SitePoint!

Smooth scrolling is a user interface mode that gradually enhances the default in-page navigation experience, animating the position within the scroll box (viewport or scrollable element), from the activation link position to the link URL The position of the target element indicated in the clip.

This is nothing new and has been a known pattern for years, for example, check out this SitePoint article dating back to 2003! By the way, this post is historically valuable because it shows how client-side JavaScript programming, especially DOM, has changed and evolved over the years, allowing for the development of easier native JavaScript solutions.

In the jQuery ecosystem, there are many implementations of this pattern, which can be implemented directly with jQuery or with plug-ins, but in this article, we are interested in pure JavaScript solutions. Specifically, we will explore and utilize the Jump.js library.

After introducing an overview of the library and its features and features, we will make some changes to the original code to suit our needs. In the process, we will review some core JavaScript language skills such as functions and closures. We will then create an HTML page to test the smooth scrolling behavior and then implement it as a custom script. Support for native smooth scrolling in CSS will then be added (if available), and finally we will make some observations on the browser navigation history.

This is the final demonstration we will create:

View Smooth Scrolling Pen for SitePoint (@SitePoint) on CodePen.

The complete source code can be found on GitHub.

Jump.js

Jump.js is written in native ES6 JavaScript and has no external dependencies. It is a small utility with only about 42 SLOC, but the minimization package provided is about 2.67 KB in size, as it has to be translated. A demo is provided on the GitHub project page.

As the name implies, it only provides jumps: the animation changes of the scroll bar position from its current value to the target position, specified by providing distances in the form of a DOM element, a CSS selector, or positive or negative numerical value. This means that in the implementation of smooth scroll mode, we have to perform link hijacking ourselves. See the section below for more information.

Please note that only vertical scrolling of the viewport is supported at present.

We can configure jumps with some options, such as duration (this parameter is required), easing function, and callbacks triggered at the end of the animation. We'll see their practical application later in the demo. See the documentation for complete details.

Jump.js runs on "modern" browsers without any problem, including Internet Explorer version 10 or later. Again, please refer to the documentation for a complete list of supported browsers. With the appropriate requestAnimationFrame polyfill, it can even run on older browsers.

Quickly understand behind the screen

Internally, the Jump.js source code uses the requestAnimationFrame method of the window object to arrange the position of the viewport vertical position updated in each frame of the scroll animation. This update is achieved by passing the next position value calculated using the easing function to the window.scrollTo method. See the source code for complete details.

Some customizations

We'll make some minor changes to the original code before delving into the demo to show how Jump.js is used, but that won't modify how it works internally.

The source code is written in ES6 and needs to be used with JavaScript build tools to translate and bundle modules. This may be a bit too much for some projects, so we will apply some refactoring to convert the code to ES5 for use anywhere.

First, let's remove ES6 syntax and features. The script defines an ES6 class:

<code>import easeInOutQuad from './easing'

export default class Jump {
  jump(target, options = {}) {
    this.start = window.pageYOffset

    this.options = {
      duration: options.duration,
      offset: options.offset || 0,
      callback: options.callback,
      easing: options.easing || easeInOutQuad
    }

    this.distance = typeof target === 'string'
      ? this.options.offset + document.querySelector(target).getBoundingClientRect().top
      : target

    this.duration = typeof this.options.duration === 'function'
      ? this.options.duration(this.distance)
      : this.options.duration

    requestAnimationFrame(time => this._loop(time))
  }

  _loop(time) {
    if(!this.timeStart) {
      this.timeStart = time
    }

    this.timeElapsed = time - this.timeStart
    this.next = this.options.easing(this.timeElapsed, this.start, this.distance, this.duration)

    window.scrollTo(0, this.next)

    this.timeElapsed       ? requestAnimationFrame(time => this._loop(time))
      : this._end()
  }

  _end() {
    window.scrollTo(0, this.start + this.distance)

    typeof this.options.callback === 'function' && this.options.callback()
    this.timeStart = false
  }
}
</code>

We can convert it to ES5 "classes" using constructors and a bunch of prototype methods, but note that we never need multiple instances of this class, so a singleton implemented with a normal object literal is fine :

<code>var jump = (function() {

    var o = {

        jump: function(target, options) {
            this.start = window.pageYOffset

            this.options = {
              duration: options.duration,
              offset: options.offset || 0,
              callback: options.callback,
              easing: options.easing || easeInOutQuad
            }

            this.distance = typeof target === 'string'
              ? this.options.offset + document.querySelector(target).getBoundingClientRect().top
              : target

            this.duration = typeof this.options.duration === 'function'
              ? this.options.duration(this.distance)
              : this.options.duration

            requestAnimationFrame(_loop)
        },

        _loop: function(time) {
            if(!this.timeStart) {
              this.timeStart = time
            }

            this.timeElapsed = time - this.timeStart
            this.next = this.options.easing(this.timeElapsed, this.start, this.distance, this.duration)

            window.scrollTo(0, this.next)

            this.timeElapsed               ? requestAnimationFrame(_loop)
              : this._end()
        },

        _end: function() {
            window.scrollTo(0, this.start + this.distance)

            typeof this.options.callback === 'function' && this.options.callback()
            this.timeStart = false
        }

    };

    var _loop = o._loop.bind(o);

    // Robert Penner's easeInOutQuad - http://robertpenner.com/easing/
    function easeInOutQuad(t, b, c, d)  {
        t /= d / 2
        if(t         t--
        return -c / 2 * (t * (t - 2) - 1) + b
    }

    return o;

})();
</code>

In addition to deleting the class, we need to make some other changes. The callback of requestAnimationFrame is used to update the scrollbar position in each frame, and in the original code it is called via the ES6 arrow function, pre-bound to the jump singleton at initialization. We then bundle the default easing function in the same source file. Finally, we wrap the code using IIFE (call function expression immediately) to avoid namespace pollution.

Now we can apply another reconstruction step, note that with the help of nested functions and closures, we can just use functions instead of objects:

<code>function jump(target, options) {
    var start = window.pageYOffset;

    var opt = {
      duration: options.duration,
      offset: options.offset || 0,
      callback: options.callback,
      easing: options.easing || easeInOutQuad
    };

    var distance = typeof target === 'string' ? 
        opt.offset + document.querySelector(target).getBoundingClientRect().top : 
        target
    ;

    var duration = typeof opt.duration === 'function'
          ? opt.duration(distance)
          : opt.duration
    ;

    var 
        timeStart = null,
        timeElapsed
    ;

    requestAnimationFrame(loop);

    function loop(time) {
        if (timeStart === null)
            timeStart = time;

        timeElapsed = time - timeStart;

        window.scrollTo(0, opt.easing(timeElapsed, start, distance, duration));

        if (timeElapsed         requestAnimationFrame(loop)
        else
            end();
    }

    function end() {
        window.scrollTo(0, start + distance);

        typeof opt.callback === 'function' && opt.callback();
        timeStart = null;
    }

    // ...

}
</code>

Singleton is now a jump function that will be called to animate scrolling, loop and end callbacks become nested functions, and the properties of the object are now a local variable (closure). We no longer need IIFE, because now all the code is safely wrapped in a function.

As the final refactoring step, in order to avoid repeated timeStart reset checks every time the loop callback is called, the first time requestAnimationFrame() is called, we will pass it an anonymous function to it, which function is before calling the loop function Reset timerStart variable:

<code>import easeInOutQuad from './easing'

export default class Jump {
  jump(target, options = {}) {
    this.start = window.pageYOffset

    this.options = {
      duration: options.duration,
      offset: options.offset || 0,
      callback: options.callback,
      easing: options.easing || easeInOutQuad
    }

    this.distance = typeof target === 'string'
      ? this.options.offset + document.querySelector(target).getBoundingClientRect().top
      : target

    this.duration = typeof this.options.duration === 'function'
      ? this.options.duration(this.distance)
      : this.options.duration

    requestAnimationFrame(time => this._loop(time))
  }

  _loop(time) {
    if(!this.timeStart) {
      this.timeStart = time
    }

    this.timeElapsed = time - this.timeStart
    this.next = this.options.easing(this.timeElapsed, this.start, this.distance, this.duration)

    window.scrollTo(0, this.next)

    this.timeElapsed       ? requestAnimationFrame(time => this._loop(time))
      : this._end()
  }

  _end() {
    window.scrollTo(0, this.start + this.distance)

    typeof this.options.callback === 'function' && this.options.callback()
    this.timeStart = false
  }
}
</code>

Note again that during the reconstruction process, the core scroll animation code has not changed.

Test Page

Now that we have customized the script to suit our needs, we are ready to assemble a test demo. In this section, we will write a page that enhances smooth scrolling using the scripts described in the next section.

This page contains a table of contents (TOC) that points to links within the page in the subsequent sections of the document, as well as other links to the TOC. We will also mix some external links to other pages. This is the basic structure of this page:

<code>var jump = (function() {

    var o = {

        jump: function(target, options) {
            this.start = window.pageYOffset

            this.options = {
              duration: options.duration,
              offset: options.offset || 0,
              callback: options.callback,
              easing: options.easing || easeInOutQuad
            }

            this.distance = typeof target === 'string'
              ? this.options.offset + document.querySelector(target).getBoundingClientRect().top
              : target

            this.duration = typeof this.options.duration === 'function'
              ? this.options.duration(this.distance)
              : this.options.duration

            requestAnimationFrame(_loop)
        },

        _loop: function(time) {
            if(!this.timeStart) {
              this.timeStart = time
            }

            this.timeElapsed = time - this.timeStart
            this.next = this.options.easing(this.timeElapsed, this.start, this.distance, this.duration)

            window.scrollTo(0, this.next)

            this.timeElapsed               ? requestAnimationFrame(_loop)
              : this._end()
        },

        _end: function() {
            window.scrollTo(0, this.start + this.distance)

            typeof this.options.callback === 'function' && this.options.callback()
            this.timeStart = false
        }

    };

    var _loop = o._loop.bind(o);

    // Robert Penner's easeInOutQuad - http://robertpenner.com/easing/
    function easeInOutQuad(t, b, c, d)  {
        t /= d / 2
        if(t         t--
        return -c / 2 * (t * (t - 2) - 1) + b
    }

    return o;

})();
</code>

At the head, we will include some CSS rules to set the basic simplest layout, and at the end of the body tag, we will include two JavaScript files: the former is our refactored version of Jump.js, and the latter is It's the script we'll discuss now.

Main script

This is a script that will enhance the test page scrolling experience using animated jumps from our customized version of Jump.js library. Of course, this code will also be written in ES5 JavaScript.

Let's brief overview of what it should do: it must hijack the clicks on the link within the page, disable the browser's default behavior (suddenly jump to the hash fragment of the href attribute of the click link that clicks on the link, and replace it with a call to our jump() function.

Therefore, first of all, you need to monitor the clicks on the links in the page. We can do this in two ways, using event delegates or attaching handlers to each related link.

Event commission

In the first method, we add the click listener to an element document.body. This way, each click event of any element on the page will bubbling along its ancestor's branches into the DOM tree until it reaches document.body:

<code>function jump(target, options) {
    var start = window.pageYOffset;

    var opt = {
      duration: options.duration,
      offset: options.offset || 0,
      callback: options.callback,
      easing: options.easing || easeInOutQuad
    };

    var distance = typeof target === 'string' ? 
        opt.offset + document.querySelector(target).getBoundingClientRect().top : 
        target
    ;

    var duration = typeof opt.duration === 'function'
          ? opt.duration(distance)
          : opt.duration
    ;

    var 
        timeStart = null,
        timeElapsed
    ;

    requestAnimationFrame(loop);

    function loop(time) {
        if (timeStart === null)
            timeStart = time;

        timeElapsed = time - timeStart;

        window.scrollTo(0, opt.easing(timeElapsed, start, distance, duration));

        if (timeElapsed         requestAnimationFrame(loop)
        else
            end();
    }

    function end() {
        window.scrollTo(0, start + distance);

        typeof opt.callback === 'function' && opt.callback();
        timeStart = null;
    }

    // ...

}
</code>
Of course, now in the registered event listener (onClick), we have to check the target of the incoming click event object to check if it is related to the link element in the page. This can be done in a number of ways, so we will abstract it as a helper function isInPageLink(). We will look at the mechanism of this function later.

If the incoming click is on the in-page link, we will stop the event bubble and block the associated default action. Finally, we call the jump function, providing it with the hash selector of the target element and the parameters to configure the required animation.

This is the event handler:

<code>requestAnimationFrame(function(time) { timeStart = time; loop(time); });

function loop(time) {
    timeElapsed = time - timeStart;

    window.scrollTo(0, opt.easing(timeElapsed, start, distance, duration));

    if (timeElapsed         requestAnimationFrame(loop)
    else
        end();
}
</code>
Single handler

Use the second method to monitor link clicks, attaching the slightly modified version of the event handler described above to the link element within each page, so there is no event bubble:

<code>import easeInOutQuad from './easing'

export default class Jump {
  jump(target, options = {}) {
    this.start = window.pageYOffset

    this.options = {
      duration: options.duration,
      offset: options.offset || 0,
      callback: options.callback,
      easing: options.easing || easeInOutQuad
    }

    this.distance = typeof target === 'string'
      ? this.options.offset + document.querySelector(target).getBoundingClientRect().top
      : target

    this.duration = typeof this.options.duration === 'function'
      ? this.options.duration(this.distance)
      : this.options.duration

    requestAnimationFrame(time => this._loop(time))
  }

  _loop(time) {
    if(!this.timeStart) {
      this.timeStart = time
    }

    this.timeElapsed = time - this.timeStart
    this.next = this.options.easing(this.timeElapsed, this.start, this.distance, this.duration)

    window.scrollTo(0, this.next)

    this.timeElapsed       ? requestAnimationFrame(time => this._loop(time))
      : this._end()
  }

  _end() {
    window.scrollTo(0, this.start + this.distance)

    typeof this.options.callback === 'function' && this.options.callback()
    this.timeStart = false
  }
}
</code>

We query all elements and use the [].slice() trick to convert the returned DOM NodeList into a JavaScript array (a better alternative if the target browser supports it is to use ES6 Array.from() method). We can then use the array method to filter the links within the page, reuse the same helper function defined above, and finally attach the listener to the remaining link elements.

Event handler is almost the same as before, but of course we don't need to check the click target:

<code>var jump = (function() {

    var o = {

        jump: function(target, options) {
            this.start = window.pageYOffset

            this.options = {
              duration: options.duration,
              offset: options.offset || 0,
              callback: options.callback,
              easing: options.easing || easeInOutQuad
            }

            this.distance = typeof target === 'string'
              ? this.options.offset + document.querySelector(target).getBoundingClientRect().top
              : target

            this.duration = typeof this.options.duration === 'function'
              ? this.options.duration(this.distance)
              : this.options.duration

            requestAnimationFrame(_loop)
        },

        _loop: function(time) {
            if(!this.timeStart) {
              this.timeStart = time
            }

            this.timeElapsed = time - this.timeStart
            this.next = this.options.easing(this.timeElapsed, this.start, this.distance, this.duration)

            window.scrollTo(0, this.next)

            this.timeElapsed               ? requestAnimationFrame(_loop)
              : this._end()
        },

        _end: function() {
            window.scrollTo(0, this.start + this.distance)

            typeof this.options.callback === 'function' && this.options.callback()
            this.timeStart = false
        }

    };

    var _loop = o._loop.bind(o);

    // Robert Penner's easeInOutQuad - http://robertpenner.com/easing/
    function easeInOutQuad(t, b, c, d)  {
        t /= d / 2
        if(t         t--
        return -c / 2 * (t * (t - 2) - 1) + b
    }

    return o;

})();
</code>

Which method is best depends on the context of use. For example, if new link elements may be added dynamically after the initial page loads, we must use event delegates.

Now we turn to the implementation of isInPageLink(), which we used this helper function in our previous event handler to abstract tests of in-page links. As we can see, this function takes a DOM node as a parameter and returns a Boolean value to indicate whether the node represents an in-page link element. It is not enough to just check that the passed node is an A tag and that hash fragment is set, because the link may be pointing to another page, in which case the default browser action must not be disabled. Therefore, we check if the value "minus" hash fragment stored in the property href is equal to the page URL:

<code>function jump(target, options) {
    var start = window.pageYOffset;

    var opt = {
      duration: options.duration,
      offset: options.offset || 0,
      callback: options.callback,
      easing: options.easing || easeInOutQuad
    };

    var distance = typeof target === 'string' ? 
        opt.offset + document.querySelector(target).getBoundingClientRect().top : 
        target
    ;

    var duration = typeof opt.duration === 'function'
          ? opt.duration(distance)
          : opt.duration
    ;

    var 
        timeStart = null,
        timeElapsed
    ;

    requestAnimationFrame(loop);

    function loop(time) {
        if (timeStart === null)
            timeStart = time;

        timeElapsed = time - timeStart;

        window.scrollTo(0, opt.easing(timeElapsed, start, distance, duration));

        if (timeElapsed         requestAnimationFrame(loop)
        else
            end();
    }

    function end() {
        window.scrollTo(0, start + distance);

        typeof opt.callback === 'function' && opt.callback();
        timeStart = null;
    }

    // ...

}
</code>

striphash() is another helper function, which we also use to set the value of the variable pageUrl when the script is initialized:

<code>requestAnimationFrame(function(time) { timeStart = time; loop(time); });

function loop(time) {
    timeElapsed = time - timeStart;

    window.scrollTo(0, opt.easing(timeElapsed, start, distance, duration));

    if (timeElapsed         requestAnimationFrame(loop)
    else
        end();
}
</code>

This string-based solution and the pruning of hash fragments works fine even on URLs with query strings, because the hash parts are behind them in the general structure of the URL.

As I said before, this is just one possible way to implement this test. For example, the article cited at the beginning of this tutorial uses a different solution to perform component-level comparisons of link hrefs to location objects.

It should be noted that we use this function in both event subscription methods, but in the second method we use it as a filter for elements that we already know are tags, so The first check of the tagName property is redundant. This is left to the reader as an exercise.

Accessibility considerations

For now, our code is susceptible to known errors (actually a pair of unrelated errors that affect Blink/WebKit/KHTML and one that affects IE) that affects keyboard users. When browsing the TOC link through the tab key, activating a link will smoothly scroll down to the selected section, but the focus will remain on the link. This means that when the next tab key is pressed, the user will be sent back to the TOC instead of the first link in the section of their choice.

To solve this problem, we will add another function to the main script:

<code>>
    <h1>></h1>Title>
    <nav> id="toc"></nav>
        <ul>></ul>
            <li>></li>
<a> href="https://www.php.cn/link/db8229562f80fbcc7d780f571e5974ec"></a>Section 1>>
            <li>></li>
<a> href="https://www.php.cn/link/ba2cf4148007ed8a8b041f8abd9bbf96"></a>Section 2>>
            ...
        >
    >
     id="sect-1">
        <h2>></h2>Section 1>
        <p>></p>Pellentesque habitant morbi tristique senectus et netus et <a> href="https://www.php.cn/link/e1b97c787a5677efa5eba575c41e8688"></a>a link to another page> ac turpis egestas. <a> href="https://www.php.cn/link/e1b97c787a5677efa5eba575c41e8688index.html#foo"></a>A link to another page, with an anchor> quam, feugiat vitae, ...>
        <a> href="https://www.php.cn/link/7421d74f57142680e679057ddc98edf5"></a>Back to TOC>
    >
     id="sect-2">
        <h2>></h2>Section 2>
        ...
    >
    ...
     src="jump.js">>
     src="script.js">>
>
</code>

It will run in the callback we will pass to the jump function and pass the hash of the element we want to scroll to past:

<code>import easeInOutQuad from './easing'

export default class Jump {
  jump(target, options = {}) {
    this.start = window.pageYOffset

    this.options = {
      duration: options.duration,
      offset: options.offset || 0,
      callback: options.callback,
      easing: options.easing || easeInOutQuad
    }

    this.distance = typeof target === 'string'
      ? this.options.offset + document.querySelector(target).getBoundingClientRect().top
      : target

    this.duration = typeof this.options.duration === 'function'
      ? this.options.duration(this.distance)
      : this.options.duration

    requestAnimationFrame(time => this._loop(time))
  }

  _loop(time) {
    if(!this.timeStart) {
      this.timeStart = time
    }

    this.timeElapsed = time - this.timeStart
    this.next = this.options.easing(this.timeElapsed, this.start, this.distance, this.duration)

    window.scrollTo(0, this.next)

    this.timeElapsed       ? requestAnimationFrame(time => this._loop(time))
      : this._end()
  }

  _end() {
    window.scrollTo(0, this.start + this.distance)

    typeof this.options.callback === 'function' && this.options.callback()
    this.timeStart = false
  }
}
</code>

The function of this function is to get the DOM element corresponding to the hash value and test whether it is already an element that can receive focus (such as an anchor or button element). If the element cannot receive focus by default (such as our container), it sets its tabIndex property to -1 (allows to receive focus programmatically, but not via the keyboard). The focus will then be set to that element, which means that the user's next tab key moves the focus to the next available link.

You can view the full source code of the main script here, with all the changes discussed previously.

Support native smooth scrolling using CSS

The CSS Object Model View Module specification introduces a new property to natively implement smooth scrolling:

. scroll-behavior

It can take two values,

represents the default instantaneous scroll, and auto represents the animation scroll. This specification does not provide any way to configure scroll animations, such as its duration and time functions (easing). smooth

Can I use css-scroll-behavior? Data from caniuse.com shows the support of css-scroll-behavior functionality by major browsers.

Unfortunately, at the time of writing, support is very limited. In Chrome, this feature is under development and can be partially implemented by enabling it in the chrome://flags screen. The CSS property has not been implemented yet, so smooth scrolling on link clicks does not work.

Anyway, by making small changes to the main script, we can detect if this feature is available in the user agent and avoid running the rest of our code. To use smooth scrolling in the viewport, we apply the CSS attribute to the root element HTML (but in our test page we can even apply it to the body element):

<code>var jump = (function() {

    var o = {

        jump: function(target, options) {
            this.start = window.pageYOffset

            this.options = {
              duration: options.duration,
              offset: options.offset || 0,
              callback: options.callback,
              easing: options.easing || easeInOutQuad
            }

            this.distance = typeof target === 'string'
              ? this.options.offset + document.querySelector(target).getBoundingClientRect().top
              : target

            this.duration = typeof this.options.duration === 'function'
              ? this.options.duration(this.distance)
              : this.options.duration

            requestAnimationFrame(_loop)
        },

        _loop: function(time) {
            if(!this.timeStart) {
              this.timeStart = time
            }

            this.timeElapsed = time - this.timeStart
            this.next = this.options.easing(this.timeElapsed, this.start, this.distance, this.duration)

            window.scrollTo(0, this.next)

            this.timeElapsed               ? requestAnimationFrame(_loop)
              : this._end()
        },

        _end: function() {
            window.scrollTo(0, this.start + this.distance)

            typeof this.options.callback === 'function' && this.options.callback()
            this.timeStart = false
        }

    };

    var _loop = o._loop.bind(o);

    // Robert Penner's easeInOutQuad - http://robertpenner.com/easing/
    function easeInOutQuad(t, b, c, d)  {
        t /= d / 2
        if(t         t--
        return -c / 2 * (t * (t - 2) - 1) + b
    }

    return o;

})();
</code>
Then, we add a simple functional detection test at the beginning of the script:

<code>function jump(target, options) {
    var start = window.pageYOffset;

    var opt = {
      duration: options.duration,
      offset: options.offset || 0,
      callback: options.callback,
      easing: options.easing || easeInOutQuad
    };

    var distance = typeof target === 'string' ? 
        opt.offset + document.querySelector(target).getBoundingClientRect().top : 
        target
    ;

    var duration = typeof opt.duration === 'function'
          ? opt.duration(distance)
          : opt.duration
    ;

    var 
        timeStart = null,
        timeElapsed
    ;

    requestAnimationFrame(loop);

    function loop(time) {
        if (timeStart === null)
            timeStart = time;

        timeElapsed = time - timeStart;

        window.scrollTo(0, opt.easing(timeElapsed, start, distance, duration));

        if (timeElapsed         requestAnimationFrame(loop)
        else
            end();
    }

    function end() {
        window.scrollTo(0, start + distance);

        typeof opt.callback === 'function' && opt.callback();
        timeStart = null;
    }

    // ...

}
</code>
So if the browser supports native scrolling, the script will not do anything and exit, otherwise it will continue to execute as before and the browser will ignore unsupported CSS properties.

Conclusion

Apart from the simplicity and performance implementation, another advantage of the CSS solution just discussed is that the browser history behavior is consistent with the behavior experienced when using the browser's default scrolling. Each in-page jump is pushed to the browser history stack, and we can browse these jumps back and forth with the corresponding buttons (but at least there is no smooth scrolling in Firefox).

In the code we wrote (we can now think of it as a fallback scheme when CSS support is not available), we did not consider the behavior of scripts relative to browser history. Depending on the context and use case, this may or may not be something of interest, but if we think scripts should enhance the default scrolling experience, then we should expect consistent behavior, just like CSS.

FAQs (FAQs) on Smooth Scrolling with Native JavaScript

How to achieve smooth scrolling using native JavaScript without using any libraries?

Use native JavaScript to achieve smooth scrolling without using any libraries is very simple. You can use the window.scrollTo method and set the behavior option to smooth. This method scrolls documents in the window by a given number of times. Here is a simple example:

<code>import easeInOutQuad from './easing'

export default class Jump {
  jump(target, options = {}) {
    this.start = window.pageYOffset

    this.options = {
      duration: options.duration,
      offset: options.offset || 0,
      callback: options.callback,
      easing: options.easing || easeInOutQuad
    }

    this.distance = typeof target === 'string'
      ? this.options.offset + document.querySelector(target).getBoundingClientRect().top
      : target

    this.duration = typeof this.options.duration === 'function'
      ? this.options.duration(this.distance)
      : this.options.duration

    requestAnimationFrame(time => this._loop(time))
  }

  _loop(time) {
    if(!this.timeStart) {
      this.timeStart = time
    }

    this.timeElapsed = time - this.timeStart
    this.next = this.options.easing(this.timeElapsed, this.start, this.distance, this.duration)

    window.scrollTo(0, this.next)

    this.timeElapsed       ? requestAnimationFrame(time => this._loop(time))
      : this._end()
  }

  _end() {
    window.scrollTo(0, this.start + this.distance)

    typeof this.options.callback === 'function' && this.options.callback()
    this.timeStart = false
  }
}
</code>

In this example, when you click on an element with class your-element, the page will scroll smoothly to the top.

Why does my smooth scroll not work in Safari?

Smooth scrolling function using the scrollTo method and setting the behavior option to smooth is not supported in Safari. To make it work you can use polyfill, such as smoothscroll-polyfill. This will enable smooth scrolling in browsers that do not support it natively.

How to scroll smoothly to a specific element?

To scroll smoothly to a specific element, you can use the Element.scrollIntoView method and set the behavior option to smooth. Here is an example:

<code>var jump = (function() {

    var o = {

        jump: function(target, options) {
            this.start = window.pageYOffset

            this.options = {
              duration: options.duration,
              offset: options.offset || 0,
              callback: options.callback,
              easing: options.easing || easeInOutQuad
            }

            this.distance = typeof target === 'string'
              ? this.options.offset + document.querySelector(target).getBoundingClientRect().top
              : target

            this.duration = typeof this.options.duration === 'function'
              ? this.options.duration(this.distance)
              : this.options.duration

            requestAnimationFrame(_loop)
        },

        _loop: function(time) {
            if(!this.timeStart) {
              this.timeStart = time
            }

            this.timeElapsed = time - this.timeStart
            this.next = this.options.easing(this.timeElapsed, this.start, this.distance, this.duration)

            window.scrollTo(0, this.next)

            this.timeElapsed               ? requestAnimationFrame(_loop)
              : this._end()
        },

        _end: function() {
            window.scrollTo(0, this.start + this.distance)

            typeof this.options.callback === 'function' && this.options.callback()
            this.timeStart = false
        }

    };

    var _loop = o._loop.bind(o);

    // Robert Penner's easeInOutQuad - http://robertpenner.com/easing/
    function easeInOutQuad(t, b, c, d)  {
        t /= d / 2
        if(t         t--
        return -c / 2 * (t * (t - 2) - 1) + b
    }

    return o;

})();
</code>

In this example, when you click on an element with class your-element, the page will smoothly scroll to an element with class target-element.

Can I control the speed of smooth scrolling?

The speed of smooth scrolling cannot be directly controlled because it is handled by the browser. However, you can use window.requestAnimationFrame to create a custom smooth scroll function for better control over the scrolling animation, including its speed.

How to achieve horizontal smooth scrolling?

You can achieve horizontal smooth scrolling in a similar way to vertical smooth scrolling. The window.scrollTo and Element.scrollIntoView methods also accept the left options to specify the horizontal position to scroll to. Here is an example:

<code>function jump(target, options) {
    var start = window.pageYOffset;

    var opt = {
      duration: options.duration,
      offset: options.offset || 0,
      callback: options.callback,
      easing: options.easing || easeInOutQuad
    };

    var distance = typeof target === 'string' ? 
        opt.offset + document.querySelector(target).getBoundingClientRect().top : 
        target
    ;

    var duration = typeof opt.duration === 'function'
          ? opt.duration(distance)
          : opt.duration
    ;

    var 
        timeStart = null,
        timeElapsed
    ;

    requestAnimationFrame(loop);

    function loop(time) {
        if (timeStart === null)
            timeStart = time;

        timeElapsed = time - timeStart;

        window.scrollTo(0, opt.easing(timeElapsed, start, distance, duration));

        if (timeElapsed         requestAnimationFrame(loop)
        else
            end();
    }

    function end() {
        window.scrollTo(0, start + distance);

        typeof opt.callback === 'function' && opt.callback();
        timeStart = null;
    }

    // ...

}
</code>

This will make the document smoothly scroll 100 pixels to the right.

How to stop smooth scrolling animation?

The smooth scrolling animation cannot be stopped directly because it is handled by the browser. However, if you are using a custom smooth scroll function, you can use window.cancelAnimationFrame to cancel the animation frame to stop the animation.

How to achieve smooth scrolling with fixed headers?

To achieve smooth scrolling with fixed headers, you need to adjust the scroll position to take into account the height of the header. You can do this by subtracting the height of the header from the target scroll position.

How to achieve smooth scrolling for anchor links?

To achieve smooth scrolling for anchor links, you can add event listeners to the link's click event and use the Element.scrollIntoView method to scroll smoothly to the target element. Here is an example:

<code>requestAnimationFrame(function(time) { timeStart = time; loop(time); });

function loop(time) {
    timeElapsed = time - timeStart;

    window.scrollTo(0, opt.easing(timeElapsed, start, distance, duration));

    if (timeElapsed         requestAnimationFrame(loop)
    else
        end();
}
</code>

This will smoothly scroll all anchor links on the page to its target element.

How to use keyboard navigation to achieve smooth scrolling?

Using keyboard navigation to achieve smooth scrolling is more complicated because it requires intercepting keyboard events and manually scrolling documents. You can do this by adding an event listener to the keydown event and scrolling the document smoothly using the window.scrollTo method.

How to test the compatibility of my smooth scroll implementation?

You can use online tools such as BrowserStack to test the compatibility of smooth scrolling. These tools allow you to test your website on different browsers and on different devices to ensure your implementation works properly across all environments.

The above is the detailed content of How to Implement Smooth Scrolling in Vanilla JavaScript. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn