Highlight Active Menu Item on Scroll

Highlight my navbar menu items whenever I scroll through that section? Only using Javascript

Just utilize the mouseover and mouseout events!

Here is a small example & here is a JS Fiddle as well:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test</title>
<style>
body {
font-family : "Arial", sans-serif;
}

#navbar__list {
height : 40px;
background-color : #55443D;
display : block;
align-content : center;
align-items : center;
position : fixed;
top : 0;
left : 0;
width : 100vw;
z-index : 1;
margin : 0 auto;

}

#navbar__list ul {
padding : 0;
list-style : none;
position : relative;
display : table;
margin : 0 auto;
}

#navbar__list li {
display : table-cell;
}

#navbar__list li a {
padding : 10px 20px;
display : block;
color : white;
text-decoration : none;
transition : all 0.3s ease-in-out;
}

#navbar__list li a:hover {
color : #dc5c26;
}

#navbar__list li a .active {
color : #F38A8A;
border-bottom : 3px solid #F38A8A;
}

.active {
color : #F38A8A;
border-bottom : 3px solid #F38A8A;
}

.sec {
height : 50vh;
display : block;
}

.sec h2 {
position : relative;
top : 50%;
left : 50%;
}

#section1 {
background-color : green;
}

#section2 {
background-color : yellow;
}

#section3 {
background-color : blue;
}

#section4 {
background-color : grey;
}
</style>
</head>
<body>
<ul id="navbar__list"></ul>

<section class="container">
<div id="section1" class="sec">
<h2>Section 1</h2>
</div>
<div id="section2" class="sec">
<h2>Section 2</h2>
</div>
<div id="section3" class="sec">
<h2>Section 3</h2>
</div>
<div id="section4" class="sec">
<h2>Section 4</h2>
</div>
</section>
</body>
<script>
const navMenu = document.querySelectorAll( "section" );
const navList = document.getElementById( "navbar__list" );
const items = [ "Section 1", "Section 2", "Section 3", "Section 4" ];
let lastId;
let last_known_scroll_position = 0;
let ticking = false;

//Build the nav
items.forEach( ( item, i ) => {
const li = document.createElement( "li" );
const el = document.createElement( "a" );
el.innerText = item;
el.classList.add( "menu-items" );
el.setAttribute( "id", `menu-${i + 1}` );
el.href = `#section${i + 1}`;

el.addEventListener( "click", function ( e ) {
const href = e.target.getAttribute( "href" ),
offsetTop = href === "#" ? 0 : e.target.offsetTop - topMenuHeight + 1;
const scrollOptions = { scrollIntoView: true, behavior: "smooth" };
e.target.scrollIntoView( scrollOptions );
e.preventDefault();
} );

navList.appendChild( li );
li.appendChild( el );
} );

const topMenu = document.getElementById( "navbar__list" );
const topMenuHeight = topMenu.offsetHeight + 1;
const menuItems = document.querySelectorAll( ".menu-items" );
const scrollItems = document.querySelectorAll( ".sec" );

//Make Nav Active when Clicked and scrolls down to section
document.addEventListener( "click", function ( event ) {
let active = document.querySelector( ".active" );
if ( active ) {
active.classList.remove( "active" );
}
if ( event.target.classList.contains( "menu-items" ) ) {
event.target.classList.add( "active" );
}
} );

// Bind to scroll
window.addEventListener( "scroll", function () {
// Get container scroll position
const container = document.querySelector( ".container" );
let fromTop = window.pageYOffset + topMenuHeight + 40;

// Get id of current scroll item
let cur = [];

[ ...scrollItems ].map( function ( item ) {
if ( item.offsetTop < fromTop ) {
cur.push( item );
}
} );

// Get the id of the current element
cur = cur[ cur.length - 1 ];
let id = cur ? cur.id : "";

if ( lastId !== id ) {
lastId = id;

menuItems.forEach( function ( elem, index ) {
elem.classList.remove( "active" );
const filteredItems = [ ...menuItems ].filter( elem => elem.getAttribute( "href" ) === `#${id}` );
filteredItems[ 0 ].classList.add( "active" );
} );
}
} );
</script>
</html>

Edit: This is the working fiddle using your code

Highlight Menu Item when Scrolling Down to Section

EDIT:

I have modified my answer to talk a little about performance and some particular cases.

If you are here just looking for code, there is a commented snippet at the bottom.


Original answer

Instead of adding the .active class to all the links, you should identify the one which attribute href is the same as the section's id.

Then you can add the .active class to that link and remove it from the rest.

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

With the above modification your code will correctly highlight the corresponding link. Hope it helps!


Improving performance

Even when this code will do its job, is far from being optimal. Anyway, remember:

We should forget about small efficiencies, say about 97% of the time:
premature optimization is the root of all evil. Yet we should not pass
up our opportunities in that critical 3%. (Donald Knuth)

So if, event testing in a slow device, you experience no performance issues, the best you can do is to stop reading and to think about the next amazing feature for your project!

There are, basically, three steps to improve the performance:

Make as much previous work as possible:

In order to avoid searching the DOM once and again (each time the event is triggered), you can cache your jQuery objects beforehand (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 anchor selector: the hash '#' has a special meaning in CSS so it must be escaped (thanks @Johnnie).

Also, you could cache the position of each section (Bootstrap's Scrollspy does it). But, if you do it, you need to remember to update them every time they change (the user resizes the window, new content is added via ajax, a subsection is expanded, etc).

Optimize the event handler:

Imagine that the user is scrolling inside one section: the active navigation link doesn't need to change. But if you look at the code above you will see that actually it changes several times. Before the correct link get highlighted, all the previous links will do it as well (because their corresponding sections also validate the condition position >= target).

One solution is to iterate the sections for the bottom to the top, the first one whose .offset().top is equal or smaller than $(window).scrollTop is the correct one. And yes, you can rely on jQuery returning the objects in the order of the DOM (since version 1.3.2). To iterate from bottom to top just select them in inverse order:

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

The double $() is 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 avoid to check further sections.

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

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

Trigger the event as less as possible:

The most definitive way to prevent high-rated events (scroll, resize...) from making your site slow or unresponsive is to control how often the event handler is called: sure you don't need to check which link needs to be highlighted 100 times per second! If, besides the link highlighting, you add some fancy parallax effect you can ran fast intro troubles.

At this point, sure you want to read about throttle, debounce and requestAnimationFrame. This article is a nice lecture and give you a very good overview about three of them. For our case, throttling fits best our needs.

Basically, throttling enforces a minimum time interval between two function executions.

I have implemented a throttle function in the snippet. From there you can get more sophisticated, or even better, use a library like underscore.js or lodash (if you don't need the whole library you can always extract from there the throttle function).

Note: if you look around, you will find more simple throttle functions. Beware of them because they can miss the last event trigger (and that is the most important one!).

Particular cases:

I will not include these cases in the snippet, to not complicate it any further.

In the snippet below, the links will get highlighted when the section reaches the very top of the page. If you want them highlighted before, you can add a small offset in this way:

if (position + offset >= target) {

This is particullary 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 hightlight its corresponding link when the scrollbar is in its bottom-most position:

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

There are some browser support issues thought. You can read more about it here and here.

Snippet and test

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

// cache the navigation links var $navigationLinks = $('#navigation > ul > li > a');// cache (in reversed order) the sectionsvar $sections = $($(".section").get().reverse());
// map each section id to their corresponding navigation linkvar sectionIdTonavigationLink = {};$sections.each(function() { var id = $(this).attr('id'); sectionIdTonavigationLink[id] = $('#navigation > ul > li > a[href=\\#' + id + ']');});
// throttle function, enforces a minimum time intervalfunction 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>

Highlighting item in table of contents when section is active on page as scrolling

I modified randomdude's answer to highlight the lowest scrolled-to header. This will persist highlighting of that link until the user scrolls down far enough to another one.

const anchors = $('body').find('h1');

$(window).scroll(function(){
var scrollTop = $(document).scrollTop();

// highlight the last scrolled-to: set everything inactive first
for (var i = 0; i < anchors.length; i++){
$('nav ul li a[href="#' + $(anchors[i]).attr('id') + '"]').removeClass('active');
}

// then iterate backwards, on the first match highlight it and break
for (var i = anchors.length-1; i >= 0; i--){
if (scrollTop > $(anchors[i]).offset().top - 75) {
$('nav ul li a[href="#' + $(anchors[i]).attr('id') + '"]').addClass('active');
break;
}
}
});

forked fiddle link: http://jsfiddle.net/tz6yxfk3/

Highlight the corresponding menu item of active HTML section

try to make your code like this. I hope this is the helpful to you.

$(document).ready(function(){  var lastId,  topMenu = $(".menu_links"),  menuItems = topMenu.find("a"),  scrollItems = menuItems.map(function () {    var item = $($(this).attr("href"));    if (item.length) {      return item;    }  });  menuItems.click(function (e) {    var href = $(this).attr("href"),    offsetTop = href === "#" ? 0 : $(href).offset().top + 1;    $('html, body').stop().animate({      scrollTop: offsetTop    }, 1200);    e.preventDefault();  });  $(window).scroll(function () {    var fromTop = $(this).scrollTop();    var cur = scrollItems.map(function () {      if ($(this).offset().top <= fromTop)        return this;    });    cur = cur[cur.length - 1];    var id = cur && cur.length ? cur[0].id : "";
if (lastId !== id) { lastId = id; menuItems .parent().removeClass("active") .end().filter("[href='#" + id + "']").parent().addClass("active"); } });});
body{  margin: 0;}*{  box-sizing: border-box;}section{  height: 100vh;}.menu_links{  margin: 0px;  padding: 0px;  position: fixed;  top: 0;  left: 0;  right: 0;  background-color: rgba(255,255,255,0.75);}.menu_links li{  display: inline-block;  float: left;}.menu_links li a{  padding: 10px;  color: #000;  text-decoration: none;  display: inline-block;}.menu_links li.active a{  background-color: blue;  color: #fff;}
<body>  <ul class="menu_links">    <li class="active"><a href="#section1">section1</a></li>    <li><a href="#section2">section2</a></li>    <li><a href="#section3">section3</a></li>    <li><a href="#section4">section4</a></li>  </ul>  <section id="section1" style="background-color: pink"></section>  <section id="section2" style="background-color: red"></section>  <section id="section3" style="background-color: orange"></section>  <section id="section4" style="background-color: green"></section>  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script></body>

how to highlight navigation bar while scrolling

You should apply active class to only the item that matches the current section of the page. Try this: -

function isInView(element) {
const rect = element.getBoundingClientRect();
if(rect.top > 70 && rect.top < 150){ //Assumed Y-offset for 1st section
return element.id //Assuming it returns 1 for 1st element
}
else if(rect.top > 150){
return element.id //Returns 2 for 2nd element
}

};

function addActive() {
document.addEventListener(
"scroll",
() => {

for (section of sections) {
if(isInView(section) == section.id){
section.classList.add('active') //Only apply active class to this section
}
else{
section.classList.remove('active') //Remove the active class from all other sections
}
};


})
}

Confused about highlighting active menu item on scroll (Vanilla JS)

According to documentation from W3 schools, .querySelector gets the first element in the document with class equalling the one being searched for.

a[href*=#] means the querySelector is looking for all elements containing a #, and in your case 'a[href*=' + i + ']' is further specifying elements that has the current href of i.

So when you piece everything together, the code

for (i in sections) {
if (sections[i] <= scrollPosition) {
document.querySelector('.active').setAttribute('class', ' ');
document.querySelector('a[href*=' + i + ']').setAttribute('class', 'active');
}
}

looks for the element with an href equalling the current 'i' section, and sets the attribute class to active for that element.

Hope it helps!

EDIT

Whenever the window is scrolled, the following happens:

  1. 1) A variable is set called scrollPosition, which is set equal to
    the documentElement's "highest" point (or scrollTop) OR the
    document body's "highest" point (or scrollTop)
  2. Then, for i in all sections:
    if the section is less than or equal to the scrollPostition, it changes the previously active class to null and sets the current section's class to active.

It then repeats for each section, until it arrives at the current section. So technically, every section is up to the current section is made "active" at some point (hence the console logs of each section name).

Essentially, if you were to go from "Home" to "Contact", it would remove the active class from "Home" and add it to "Portfolio", then remove it from "Portfolio" and add it to "About", before finally removing it from "About" and adding it to "Contact". That way it appears to 'know' what section you're on, but it's actually going through all of them until it reaches the right one.

(Another example: If you went from "Contact" to "Portfolio", it would remove active from "Contact" and add it to "Home", realize it's still not on the right one, then remove active from "Home" and add it to "Portfolio". This probably wouldn't work as well for a site with a bunch of sections, but it appears seamless on smaller/mid-size websites!)

There might be better ways to explain this, but I hope this 'visual' helps!



Related Topics



Leave a reply



Submit