Smooth scroll to page section from menu

In this article, I will be going over a method to create a function for one-page layouts, to scroll to sections on the page.

We were building a one-page website where you had a fixed header and would be scrolling to different parts of the page when clicking on the menu items. This resulted in some problems.

  • When scrolling to a part of the page, you would be going too far, resulting in some of the content being scrolled out of view.
  • The scroll itself was rather crude, fast and bad for the user experience. Because it happened instantaneously, it would be easy for visitors to get lost in the page if they were not manually scrolling, since they would have no feel of where they were in relation to other parts of the page.

To solve these problems, we would have to create a function to make more smooth scrolling possible by using the jQuery animate function.

Prerequisites

There are some requirements to the site, in order for the samples in this article to work. These are mainly HTML structures but there are also something related to jQuery.

Browser

This function has been tested in the latest versions of: Chrome, Firefox, Opera and Safari. For Internet Explorer, it will work in version 9+. For earlier versions for Internet Explorer, the code will fall back to the regular behavior of the link, which would be content elements within the site sections (more on that later). It should be possible to adapt the program to make it scroll to the parent site section instead of the content element, but this has not been tested.

jQuery

The samples will be using jQuery in a version lower than 1.9. The reason for the version requirement, is that the jQuery.browser function has been removed in 1.9.

Fixed header

The examples i use are designed to work with a fixed header that has the following ID: "wrapper". In the code, we will be offsetting the scroll with the sum of its height and top offset. The top offset of the header is added to let it work in cases where the header aren't at the top of the page. Examples of this could be in Drupal and WordPress when logged in as admin. It will make a fixed admin bar show above our fixed header.

Content structure

This program works where the HTML of the content is structured with sections in a one-page-style layout. It could be like the following.

<div id="site-section-wrapper" >
  <div class="site-section" >
    <div id="element-to-scroll-to"></div>
  </div>
  <div class="site-section" >
    <div id="another-element-to-scroll-to"></div>
  </div>
</div>

It is a list of site sections that each contains different kinds of content. Some content that the sections will contain, are the different element that we will be scrolling the page to later on.

Menu structure

An important part of the HTML, is the menu items that will be clicked. They need to have a structure like the following.

<ul>
  <li class="menu-item" >
    <a href="#element-to-scroll-to">Item 1</a>
  </li>
  <li class="menu-item" >
    <a href="#another-element-to-scroll-to">Item 2</a>
  </li>
</ul>

We need the LI elements to have the class "menu-item", in order to target the links. One very important thing, is that we need the href attribute of the anchor element, to link to a content element within the site-section, that we will be scrolling to. The fallback behavior in the case of Internet Explorer versions below 9, is to instantly scroll to these content elements.

Walkthrough

In the below section, we will be going through the different parts of the code. In the end, all the different pieces will be but together and the final code is shown.

Menu link click event

The first step will be to create a click event for each menu item, which is done in the following code using the jQuery "on" function. We have also added some code to only proceed if the browser version is not IE8 or below.

jQuery('.menu-item a').on("click", function(e){
  if(!(jQuery.browser.msie && parseFloat(jQuery.browser.version)<=8)){
    //Continue if a menu item is clicked and the browser is not IE and less than 9.
  }
});

Prevent default behaviour

Inside the if statement, we will then be adding the following code to prevent the default behavior that happens when the menu link is clicked, which in this case will be to instantly scroll to the content element that it links to.

e.preventDefault();

Find the site section

The next step will be to find the closest parent element of the content element, that has the class "site-section". The code for this part is seen below.

var url = e.target.href;
if (url.indexOf("#") != -1) {
  var contentElementId = url.substr(url.indexOf("#"));
  var parentSiteSection = jQuery(contentElementId).closest('.site-section');
  if (parentSiteSection.length) {
    //Continue if the section element is found.
  }
}

The above code is to be inserted directly after we have prevented the default behavior of the menu link.

The way we will be finding the site-section, is to first reference the content element. To do this, we need its ID. It should be the fragment part of the url from the link that was clicked. The clicked link can be accessed by calling "e.target", which means that we can get the url from the following line of code.

var url = e.target.href;

In order to prevent error in the case where the link has not been properly set with a fragment, we will check to see if it actually contains one, using the following lines of code.

if (url.indexOf("#") != -1) {
  //Continue if the url contains a fragment.
}

Now that we have made sure that the url contains a fragment, we will go ahead and continue with finding the site-section parent element of the desired content element.

var contentElementId = url.substr(url.indexOf("#"));
var parentSiteSection = jQuery(contentElementId).closest('.site-section');
if (parentSiteSection.length) {
  //Continue if the parent site-section is found.
}

In the above block of code, we first define the id of the content element, by getting the fragment value from the url of the link.

Next we try to find the parent section by finding the nearest element to the content element that has the "site-section" class, using the jQuery closest function.

Finally, we check the length of the new parentSiteSection variable, to make sure that we actually found something. This is to prevent any error further on.

Scrolling

And now for the scrolling part. We will be using the animate function from jQuery. The code that make the scrolling possible in this case, is seen below.

var viewport = jQuery('html, body');
viewport.animate({
scrollTop: parentSiteSection.offset().top - (jQuery('#wrapper').height()+jQuery('#wrapper').offset().top) + jQuery(document).scrollTop() + 2
}, 1000);

First we define a variable of a jQuery reference to the HTML and BODY tags. They are the ones that we will be scrolling. We need both, since browser differ on which of them they will actually be scrolling.

var viewport = jQuery('html, body');

Next we will do the animation.

viewport.animate({
  scrollTop: parentSiteSection.offset().top - (jQuery('#wrapper').height()+jQuery('#wrapper').offset().top) + jQuery(document).scrollTop()
}, 1000);

In order to scroll, we will need to calculate the position to scroll to.

The position is found by adding the top offset of the site section element and the current scroll top of the document. Since we have a fixed header, we will also need to subtract the sum of the height of the header and its top offset.

The speed of the scroll is defined by the last parameter in the animate function. In this case, it is set to 1000 ms. This can be adjusted to give the best experience on your particular site.

Stop scrolling by user interaction

We are almost finished. There are however still an issue that needs to be fixed. Using the current code, you will not be able to stop the scroll while the animation is running. Trying to do so might create a flicker.

Luckily the solution is simple and seen below.

viewport.bind("scroll mousedown DOMMouseScroll mousewheel keyup touchstart", function(e){
  if ( e.which > 0 || e.type === "mousedown" || e.type === "mousewheel" || e.type == 'touchstart'){
    viewport.stop().unbind('scroll mousedown DOMMouseScroll mousewheel keyup touchstart');
  }
});

We listens for any user interaction, and will in that case stop the current actions.

Final code

Putting all the different pieces from above together, we get the following final code.

jQuery('.menu-item a').on("click", function(e){
  if(!(jQuery.browser.msie && parseFloat(jQuery.browser.version)<=8)){
    e.preventDefault();
    var url = e.target.href;
    if (url.indexOf("#") != -1) {
      var contentElementId = url.substr(url.indexOf("#"));
      var parentSiteSection = jQuery(contentElementId).closest('.site-section');
      if (parentSiteSection.length) {
        var viewport = jQuery('html, body');
        viewport.animate({
          scrollTop: parentSiteSection.offset().top - (jQuery('#wrapper').height()+jQuery('#wrapper').offset().top) + jQuery(document).scrollTop() + 2
        }, 1000);
        viewport.bind("scroll mousedown DOMMouseScroll mousewheel keyup touchstart", function(e){
          if ( e.which > 0 || e.type === "mousedown" || e.type === "mousewheel" || e.type == 'touchstart'){
            viewport.stop().unbind('scroll mousedown DOMMouseScroll mousewheel keyup touchstart');
          }
        });
      }
    }
  }
});

 

 

 

 

 

bb@morningtrain.dk'

Bjarne Bonde