Highlight a menu item while scrolling down to a section

I know this question has been asked a million times in this forum, but not one of the articles has helped me find a solution.

I made a small piece of jquery code that highlights a hash link when scrolling down to a section with the same id as the hash link.

$(window).scroll(function() { var position = $(this).scrollTop(); $('.section').each(function() { var target = $(this).offset().top; var id = $(this).attr('id'); if (position >= target) { $('#navigation > ul > li > a').attr('href', id).addClass('active'); } }); }); 

Now the problem is that it highlights all the hash links, not just the ones the section is related to. Can someone point out a mistake, or is this what I forgot?

+11
source share
4 answers

EDIT:

I changed my answer to talk a little about performance and some specific cases.

If you're just looking for code, there is a commented snippet below.


Original answer

Instead of adding the .active class to all links, you should specify the one whose href attribute matches the section identifier.

You can then add the .active class to this link and remove it from the rest.

  if (position >= target) { $('#navigation > ul > li > a').removeClass('active'); $('#navigation > ul > li > a[href=#' + id + ']').addClass('active'); } 

Using the above modification, your code will correctly highlight the corresponding link. Hope this helps!


Performance improvement

Even when this code does its job, it is far from optimal. In any case, remember:

We must forget about low efficiency, say, in 97% of cases: premature optimization is the root of all evil. Nevertheless, we should not pass up to our capabilities in this critical 3%. (Donald Knut)

Therefore, if you have no performance problems when testing events on a slow device, the best thing you can do is to stop reading and think about the next amazing feature for your project!

There are three main steps to improving performance:

Do as many previous jobs as possible:

To avoid re-searching in the DOM (every time an event is triggered), you can pre-cache your jQuery objects (e.g. on document.ready ):

 var $navigationLinks = $('#navigation > ul > li > a'); var $sections = $(".section"); 

Then you can map each section to the corresponding navigation link:

 var sectionIdTonavigationLink = {}; $sections.each( function(){ sectionIdTonavigationLink[ $(this).attr('id') ] = $('#navigation > ul > li > a[href=\\#' + $(this).attr('id') + ']'); }); 

Note the two backslashes in the binding selector: the hash '#' has special meaning in CSS, so it needs to be escaped (thanks @Johnnie ).

In addition, you can cache the position of each section (Bootstrap Scrollspy does this). But, if you do this, you need to remember to update them every time they change (the user resizes the window, new content is added via ajax, the subsection expands, etc.).

Optimize event handler:

Imagine that a user scrolls inside one section: you do not need to change the active navigation link. But if you look at the code above, you will see that it actually changes several times. Before highlighting the correct link, all previous links will also do this (since the relevant sections also check the position >= target condition).

One solution is to iterate the sections from the bottom up, with the first one whose .offset().top is equal to or less than $(window).scrollTop is correct. And yes, you can rely on jQuery to return objects in DOM order (starting with version 1.3.2 ). To iterate from bottom to top, just select them in reverse order:

 var $sections = $( $(".section").get().reverse() ); $sections.each( ... ); 

Double $() necessary because get() returns DOM elements, not jQuery objects.

Once you have found the correct section, you should return false to exit the loop and not check other sections.

Finally, you shouldn't do anything if the correct navigation link is already highlighted, so check this out:

 if ( !$navigationLink.hasClass( 'active' ) ) { $navigationLinks.removeClass('active'); $navigationLink.addClass('active'); } 

Trigger the event as little as possible:

The most accurate way to prevent slow or unresponsive events with a high rating (scrolling, resizing ...) is to control the frequency of the event handler call: make sure that you do not need to check which link should be highlighted. 100 times per second! If, in addition to highlighting links, you add some bizarre parallax effect, you can quickly deal with troubles.

Right now, of course, you want to read about throttle, debounce, and requestAnimationFrame. This article is a good lecture and gives you a very good idea of ​​the three of them. In our case, regulation is in line with our needs.

In fact, regulation provides a minimum time interval between two executions of functions.

I implemented the throttle function in snippet. From there, you can get a more sophisticated or, better yet, a library such as underscore.js or lodash (if you don't need the whole library, you can always get the throttle function out of it).

Note: if you look around, you will find simpler gas features. Beware of them, because they can skip the last trigger of the event (and this is the most important!).

Special cases:

I will not include these cases in the fragment, so as not to complicate it.

In the snippet below, links will be highlighted when the section reaches the very top of the page. If you want them to be highlighted earlier, you can add a slight offset as follows:

 if (position + offset >= target) { 

This is especially useful when you have a top navigation bar.

And if your last section is too small to reach the top of the page, you can select the link corresponding to it when the scroll bar is in the lowest position:

 if ( $(window).scrollTop() >= $(document).height() - $(window).height() ) { // highlight the last link 

Some browser support issues have been considered. You can read more about this here and here .

Fragment and test

Finally, you have a commented snippet. Please note that I changed the name of some variables to make them more visual.

 // cache the navigation links var $navigationLinks = $('#navigation > ul > li > a'); // cache (in reversed order) the sections var $sections = $($(".section").get().reverse()); // map each section id to their corresponding navigation link var sectionIdTonavigationLink = {}; $sections.each(function() { var id = $(this).attr('id'); sectionIdTonavigationLink[id] = $('#navigation > ul > li > a[href=\\#' + id + ']'); }); // throttle function, enforces a minimum time interval function throttle(fn, interval) { var lastCall, timeoutId; return function () { var now = new Date().getTime(); if (lastCall && now < (lastCall + interval) ) { // if we are inside the interval we wait clearTimeout(timeoutId); timeoutId = setTimeout(function () { lastCall = now; fn.call(); }, interval - (now - lastCall) ); } else { // otherwise, we directly call the function lastCall = now; fn.call(); } }; } function highlightNavigation() { // get the current vertical position of the scroll bar var scrollPosition = $(window).scrollTop(); // iterate the sections $sections.each(function() { var currentSection = $(this); // get the position of the section var sectionTop = currentSection.offset().top; // if the user has scrolled over the top of the section if (scrollPosition >= sectionTop) { // get the section id var id = currentSection.attr('id'); // get the corresponding navigation link var $navigationLink = sectionIdTonavigationLink[id]; // if the link is not active if (!$navigationLink.hasClass('active')) { // remove .active class from all the links $navigationLinks.removeClass('active'); // add .active class to the current link $navigationLink.addClass('active'); } // we have found our section, so we return false to exit the each loop return false; } }); } $(window).scroll( throttle(highlightNavigation,100) ); // if you don't want to throttle the function use this instead: // $(window).scroll( highlightNavigation ); 
 #navigation { position: fixed; } #sections { position: absolute; left: 150px; } .section { height: 200px; margin: 10px; padding: 10px; border: 1px dashed black; } #section5 { height: 1000px; } .active { background: red; } 
 <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> <div id="navigation"> <ul> <li><a href="#section1">Section 1</a></li> <li><a href="#section2">Section 2</a></li> <li><a href="#section3">Section 3</a></li> <li><a href="#section4">Section 4</a></li> <li><a href="#section5">Section 5</a></li> </ul> </div> <div id="sections"> <div id="section1" class="section"> I'm section 1 </div> <div id="section2" class="section"> I'm section 2 </div> <div id="section3" class="section"> I'm section 3 </div> <div id="section4" class="section"> I'm section 4 </div> <div id="section5" class="section"> I'm section 5 </div> </div> 

And if you're interested, this fiddle checks out the various improvements that we talked about.

Good coding!

+33
source

For those who have been trying to use this solution recently, I fell into the trap of trying to get it to work. You may need to avoid href as follows:

 $('#navigation > ul > li > a[href=\\#' + id + ']'); 

And now my browser does not cause an error on this part.

+4
source

In this line:

  $('#navigation > ul > li > a').attr('href', id).addClass('active'); 

In fact, you set the href attribute for each element of $ ('# navigation> ul> li> a'), and then add the active class to all of them. Maybe you need something like:

 $('#navigation > ul > li > a[href=#' + id + ']') 

And select only a that matches the href id. It makes sense?

0
source

I took David's excellent code and removed all jQuery dependencies from it in case anyone would be interested:

 // cache the navigation links var $navigationLinks = document.querySelectorAll('nav > ul > li > a'); // cache (in reversed order) the sections var $sections = document.getElementsByTagName('section'); // map each section id to their corresponding navigation link var sectionIdTonavigationLink = {}; for (var i = $sections.length-1; i >= 0; i--) { var id = $sections[i].id; sectionIdTonavigationLink[id] = document.querySelectorAll('nav > ul > li > a[href=\\#' + id + ']') || null; } // throttle function, enforces a minimum time interval function throttle(fn, interval) { var lastCall, timeoutId; return function () { var now = new Date().getTime(); if (lastCall && now < (lastCall + interval) ) { // if we are inside the interval we wait clearTimeout(timeoutId); timeoutId = setTimeout(function () { lastCall = now; fn.call(); }, interval - (now - lastCall) ); } else { // otherwise, we directly call the function lastCall = now; fn.call(); } }; } function getOffset( el ) { var _x = 0; var _y = 0; while( el && !isNaN( el.offsetLeft ) && !isNaN( el.offsetTop ) ) { _x += el.offsetLeft - el.scrollLeft; _y += el.offsetTop - el.scrollTop; el = el.offsetParent; } return { top: _y, left: _x }; } function highlightNavigation() { // get the current vertical position of the scroll bar var scrollPosition = window.pageYOffset || document.documentElement.scrollTop; // iterate the sections for (var i = $sections.length-1; i >= 0; i--) { var currentSection = $sections[i]; // get the position of the section var sectionTop = getOffset(currentSection).top; // if the user has scrolled over the top of the section if (scrollPosition >= sectionTop - 250) { // get the section id var id = currentSection.id; // get the corresponding navigation link var $navigationLink = sectionIdTonavigationLink[id]; // if the link is not active if (typeof $navigationLink[0] !== 'undefined') { if (!$navigationLink[0].classList.contains('active')) { // remove .active class from all the links for (i = 0; i < $navigationLinks.length; i++) { $navigationLinks[i].className = $navigationLinks[i].className.replace(/ active/, ''); } // add .active class to the current link $navigationLink[0].className += (' active'); } } else { // remove .active class from all the links for (i = 0; i < $navigationLinks.length; i++) { $navigationLinks[i].className = $navigationLinks[i].className.replace(/ active/, ''); } } // we have found our section, so we return false to exit the each loop return false; } } } window.addEventListener('scroll',throttle(highlightNavigation,150)); 

0
source

All Articles