[Tutorial] Create your own a simple mouse wheel scroller like a fullpage.js with JQuery
Tags: tutorial, javascript, html, css
Published: 2020-02-01 14:18:39

:star2: Follow on [Twitter](https://twitter.com/epranka)
:star2: Connect on [LinkedIn](https://www.linkedin.com/in/edvinas-pranka-9414b31a1/)


Hi, devs

In this tutorial you will learn how to create a simple mouse wheel scroller. The following solution is very basic. It can be greatly improved, but it just an example of how fast it can be implemented or a good starting point to create your own slider.

If you need a full-featured slider, use this awesome library [fullpage.js](https://alvarotrigo.com/fullPage/) by [Alvaro Trigo](https://twitter.com/IMAC2).

There is a demo of this tutorial: [epranka.github.io/sections-slider](https://epranka.github.io/sections-slider/)

Also, this tutorial includes how to prepare a simple HTML project and run the live server.

##:anchor: How I came up with this solution?

Before several days, the client asks me to add a mouse wheel scroller to his website. I immediately thought about fullpage.js implementation. But the client website layout was "strictly" coded and he did not want to invest in changing it. So I had to come up with some dirty and fast solution without changing the entire layout. I warned the client that it is not a perfect solution, that he was fine with that.

##:hammer: Preparation

If you have the already initiated the HTML5 project with jQuery, or have your own solution to create the live server of the simple HTML5 project, skip this step :fast_forward:

If you want to follow this tutorial for the beginning, here we go :arrow_forward:

### HTML Boilerplate

First, initiate the simple HTML5 project. For this tutorial, I suggest using this awesome boilerplate called [initializr](http://www.initializr.com/). Select the **Classic H5BP** and tune the following settings:

<img src="https://i.ibb.co/ynsZDjW/image.png" alt="image" border="0" />

After the download, extract the archive. You should have the following tree in your project root:

```
.
├── css
│   ├── main.css
│   ├── normalize.css
│   └── normalize.min.css
├── img
├── js
│   ├── vendor
│   │   ├── jquery-1.11.2.min.js
│   │   └── modernizr-2.8.3.min.js
│   └── main.js
└── index.html

```

### Live server

Now it's time to make your HTML project live :alarm_clock:

Install the [http-server](https://github.com/http-party/http-server#readme) using the *npm* or *yarn*:

```bash
$ npm install -g http-server
# or
$ yarn global add http-server
```

In your project root, run the server :rocket:

```bash
# -c-1 (disable cache)
$ http-server -c-1
Starting up http-server, serving ./
Available on:
  http://127.0.0.1:8080
  http://192.168.8.10:8080
  http://192.168.250.1:8080
Hit CTRL-C to stop the server
```

Go to https://localhost:8080 in your browser and you should see the *Hello world! This is HTML5 Boilerplate.*

##:pencil2: Create the page content

Open the **index.html** and find the following line

```html
<p>Hello world! This is HTML5 Boilerplate.</p>
```

Replace it with our sections:

```html
<div id="section1" class="section">
    <span>1. Viewport height section</span>
</div>

<div id="section2" class="section">
    <span>2. Long section</span>
</div>

<div id="section3" class="section">
    <span>3. Short section</span>
</div>

<div id="section4" class="section">
    <span>4. Viewport height section</span>
</div>
```

Now in **css/main.css* find the block:

```css

/* ==========================================================================
   Author's custom styles
   ========================================================================== */
```

In this block add the styles of our content

```css

/* We center the text, make the text color
 white and increase the font size */
.section {
  color: white;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 800;
  font-size: 120%;
  font-weight: 800;
  position: relative;
}

/* The height of the first section 
 will be equal to the viewport height */
#section1 {
  height: 100vh;
  background: #6699cc;
}

/* The height of the second section
 will be 150% of the viewport height */
#section2 {
  height: 150vh;
  background: #ff8c42;
}

/* 60% height */
#section3 {
  height: 60vh;
  background: #ff3c38;
}

/* 100% (equal again) */
#section4 {
  height: 100vh;
  background: #a23e48;
}
```

Now in the browser, you should see the colorful slides:

![](https://i.ibb.co/MhS9qrb/Peek-2020-02-01-14-22.gif)

##:sparkler: Let's add some magic

All the magic goes to the **js/main.js**.

The basic idea is to collect all the sections and animate the scroll between their offsets on the mouse wheel event. So first, using the JQuery we collect all sections by *.section* class name, and define the *wheel* event handler.

```javascript
// Collecting the sections
var $sections = $(".section");

// Define wheel event handler
document.addEventListener("wheel", function(event) {

}, { passive: false });
// We set passive to false because in the handler we need to prevent the default mouse wheel behavior
```

In the handler determine the scroll direction:

```javascript
// Collecting the sections
var $sections = $(".section");

// Define wheel event handler
document.addEventListener(
  "wheel",
  function(event) {
    // Get the mouse wheel spin direction
    var direction = event.deltaY;

    if (direction > 0) {
      // Go to next
    } else {
      // Go to previous
    }
  },
  { passive: false }
);
// We set passive to false, because in the handler we need to prevent the default mouse wheel behavior

```

In the following code, we define the variable which holds the current section index and in the handler, we get the next or previous section, depending on the scroll.

```javascript
// Collecting the sections
var $sections = $(".section");

// Variable to hold the current section index
var currentIndex = 0;

// Define wheel event handler
document.addEventListener(
  "wheel",
  function(event) {
    // Get the mouse wheel spin direction
    var direction = event.deltaY;

    if (direction > 0) {
      // Go to next
      // Increase the section pointer
      currentIndex++;
      // Get the next section
      var $nextSection = $($sections[currentIndex]);
    } else {
      // Go to prev
      // Decrease the section pointer
      currentIndex--;
      // Get the previous section
      var $previousSection = $($sections[currentIndex]);
    }
  },
  { passive: false }
);
// We set passive to false, because in the handler we need to prevent the default mouse wheel behavior
```

Now we can select the section by the mouse wheel. But there is a problem. If we spin the mouse wheel too more we will get the undefined section because the pointer will be higher than the sections count. And if we spin the mouse wheel backward when we are in the first section we will get the negative pointer, which leads to the same problem: undefined section.

So we need to add the guards

```javascript
// Collecting the sections
var $sections = $(".section");

// Variable to hold the current section index
var currentIndex = 0;

// Define wheel event handler
document.addEventListener(
  "wheel",
  function(event) {
    // Get the mouse wheel spin direction
    var direction = event.deltaY;

    if (direction > 0) {
      // If next index is greater than sections count, do nothing
      if (currentIndex + 1 >= $sections.length) return;
      // Go to next
      // Increase the section pointer
      currentIndex++;
      // Get the next section
      var $nextSection = $($sections[currentIndex]);
    } else {
      // If previous index is negative, do nothing
      if (currentIndex - 1 < 0) return;
      // Go to prev
      // Decrease the section pointer
      currentIndex--;
      // Get the previous section
      var $previousSection = $($sections[currentIndex]);
    }
  },
  { passive: false }
);
// We set passive to false, because in the handler we need to prevent the default mouse wheel behavior
```

Now we are safe. Just get the offset of the next or previous section and animate scroll to it.

```javascript
// Collecting the sections
var $sections = $(".section");

// Variable to hold the current section index
var currentIndex = 0;

// Define wheel event handler
document.addEventListener(
  "wheel",
  function(event) {
    // Get the mouse wheel spin direction
    var direction = event.deltaY;

    if (direction > 0) {
      // If next index is greater than sections count, do nothing
      if (currentIndex + 1 >= $sections.length) return;
      // Go to next
      // Increase the section pointer
      currentIndex++;
      // Get the next section
      var $nextSection = $($sections[currentIndex]);
      // Get the next section offset
      var offsetTop = $nextSection.offset().top;
      // Prevent the default mouse wheel behaviour
      event.preventDefault();
      // Animate scroll
      $("html, body").animate({ scrollTop: offsetTop }, 1000);
    } else {
      // If previous index is negative, do nothing
      if (currentIndex - 1 < 0) return;
      // Go to prev
      // Decrease the section pointer
      currentIndex--;
      // Get the previous section
      var $previousSection = $($sections[currentIndex]);
      // Get the previous section offset
      var offsetTop = $previousSection.offset().top;
      // Prevent the default mouse wheel behaviour
      event.preventDefault();
      // Animate scroll
      $("html, body").animate({ scrollTop: offsetTop }, 1000);
    }
  },
  { passive: false }
);
// We set passive to false, because in the handler we need to prevent the default mouse wheel behavior
```

Tada! :tada: Now we have the working mouse wheel slider. Check it out in your browser.


Okey... I know... It has the problem again... Our slider struggles if we spin the mouse wheel too fast. But why? When you spin the mouse wheel too fast, it starts several animations before the first animation finished. So we need to skip any mouse wheel events while animating the first one.

Define the variable which holds the state of the animation. Set variable to *true* when animation starts, and - *false* when animation finished. In the event handler, if we detected that animation is in progress, we just prevent the default mouse wheel behavior.

```javascript
// Collecting the sections
var $sections = $(".section");

// Variable to hold the current section index
var currentIndex = 0;

// Variable to hold the animation state
var isAnimating = false;

// Define the animation finish callback
var stopAnimation = function() {
  // We add the 300 ms timeout to debounce the mouse wheel event
  setTimeout(function() {
    // Set the animation state to false
    isAnimating = false;
  }, 300);
};

// Define wheel event handler
document.addEventListener(
  "wheel",
  function(event) {
    // If animation is in progress
    if (isAnimating) {
      // Just prevent the default mouse wheel behaviour
      event.preventDefault();
      return;
    }

    // Get the mouse wheel spin direction
    var direction = event.deltaY;

    if (direction > 0) {
      // If next index is greater than sections count, do nothing
      if (currentIndex + 1 >= $sections.length) return;
      // Go to next
      // Increase the section pointer
      currentIndex++;
      // Get the next section
      var $nextSection = $($sections[currentIndex]);
      // Get the next section offset
      var offsetTop = $nextSection.offset().top;
      // Prevent the default mouse wheel behaviour
      event.preventDefault();
      // Set the animation state to true
      isAnimating = true;
      // Animate scroll
      $("html, body").animate({ scrollTop: offsetTop }, 1000, stopAnimation);
    } else {
      // If previous index is negative, do nothing
      if (currentIndex - 1 < 0) return;
      // Go to prev
      // Decrease the section pointer
      currentIndex--;
      // Get the previous section
      var $previousSection = $($sections[currentIndex]);
      // Get the previous section offset
      var offsetTop = $previousSection.offset().top;
      // Prevent the default mouse wheel behaviour
      event.preventDefault();
      // Set the animation state to true
      isAnimating = true;
      // Animate scroll
      $("html, body").animate({ scrollTop: offsetTop }, 1000, stopAnimation);
    }
  },
  { passive: false }
);
// We set passive to false, because in the handler we need to prevent the default mouse wheel behavior
```

I could say that is done. But I don't wanna lie. If you check our slider in the browser you will see that there no struggle anymore. But we have the last thing to do. Look at the second section (*2. Long section*). You can't scroll to the end of this section, because on the mouse wheel spin, section 3 (*3. Short section*) comes in to view.

To fix this, we should prevent the slide to the next section if we don't reach the current section bottom and vice versa, we should prevent the slide to the previous section if we don't reach the current section top.

Define the two functions

```javascript
// Function returns true if DOM element bottom is reached
var bottomIsReached = function($elem) {
  var rect = $elem[0].getBoundingClientRect();
  return rect.bottom <= $(window).height();
};

// Function returns true if DOM element top is reached
var topIsReached = function($elem) {
  var rect = $elem[0].getBoundingClientRect();
  return rect.top >= 0;
};
```

In the handler add the logic which prevents the slide as mentioned above.

```javascript
// Collecting the sections
var $sections = $(".section");

// Variable to hold the current section index
var currentIndex = 0;

// Variable to hold the animation state
var isAnimating = false;

// Define the animation finish callback
var stopAnimation = function() {
  // We add the 300 ms timeout to debounce the mouse wheel event
  setTimeout(function() {
    // Set the animation state to false
    isAnimating = false;
  }, 300);
};

// Function returns true if DOM element bottom is reached
var bottomIsReached = function($elem) {
  var rect = $elem[0].getBoundingClientRect();
  return rect.bottom <= $(window).height();
};

// Function returns true if DOM element top is reached
var topIsReached = function($elem) {
  var rect = $elem[0].getBoundingClientRect();
  return rect.top >= 0;
};

// Define wheel event handler
document.addEventListener(
  "wheel",
  function(event) {
    // If animation is in progress
    if (isAnimating) {
      // Just prevent the default mouse wheel behaviour
      event.preventDefault();
      return;
    }

    // Get the current section
    var $currentSection = $($sections[currentIndex]);

    // Get the mouse wheel spin direction
    var direction = event.deltaY;

    if (direction > 0) {
      // If next index is greater than sections count, do nothing
      if (currentIndex + 1 >= $sections.length) return;
      // If bottom is not reached allow the default behaviour
      if (!bottomIsReached($currentSection)) return;
      // Go to next
      // Increase the section pointer
      currentIndex++;
      // Get the next section
      var $nextSection = $($sections[currentIndex]);
      // Get the next section offset
      var offsetTop = $nextSection.offset().top;
      // Prevent the default mouse wheel behaviour
      event.preventDefault();
      // Set the animation state to true
      isAnimating = true;
      // Animate scroll
      $("html, body").animate({ scrollTop: offsetTop }, 1000, stopAnimation);
    } else {
      // If previous index is negative, do nothing
      if (currentIndex - 1 < 0) return;
      // If top is not reached allow the default behaviour
      if (!topIsReached($currentSection)) return;
      // Go to prev
      // Decrease the section pointer
      currentIndex--;
      // Get the previous section
      var $previousSection = $($sections[currentIndex]);
      // Get the previous section offset
      var offsetTop = $previousSection.offset().top;
      // Prevent the default mouse wheel behaviour
      event.preventDefault();
      // Set the animation state to true
      isAnimating = true;
      // Animate scroll
      $("html, body").animate({ scrollTop: offsetTop }, 1000, stopAnimation);
    }
  },
  { passive: false }
);
// We set passive to false, because in the handler we need to prevent the default mouse wheel behavior
```

Check the result in your browser.

##:heavy_check_mark: You reached the bottom!

It is far from perfect, but I can say that is done for this tutorial. And the idea is explained. I think you are strong enough to improve this to the perfect :wink:

You can check the full source code in my GitHub repository [epranka/sections-slider](https://github.com/epranka/sections-slider)

Thanks for reading this. I hope it was helpful to you. Feedback and questions are appreciated.