I’m new to vanilla js. I have a navbar with links to sections. I want to make the class active as soon as the section becomes active. If there are no active section, then remove the active class. Found such a script, but there is one drawback. If I am in an inactive section, the active class will remain with the previous active section.
const links = document.querySelectorAll('.nav-link'); const sections = document.querySelectorAll('.forJS'); function changeLinkState() { let index = sections.length; while(--index && window.scrollY + 50 < sections[index].offsetTop) {} links.forEach((link) => link.classList.remove('active')); links[index].classList.add('active'); } changeLinkState(); window.addEventListener('scroll', changeLinkState);
section{ height:100vh; scroll-y:auto; } .nav-link.active{ color: red; }
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/js/bootstrap.min.js"></script> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/css/bootstrap.min.css" rel="stylesheet"/> <body> <header class="fixed-top"> <nav class="navbar navbar-expand-lg navCustom"> <div class="container"> <ul class="navbar-nav justify-content-center"> <li class="nav-item"> <a class="nav-link" href="#main">Main</a> </li> <li class="nav-item"> <a class="nav-link" href="#about">About us</a> </li> <li class="nav-item"> <a class="nav-link" href="#portfolio">Portfolio</a> </li> <li class="nav-item"> <a class="nav-link" href="#contacts">Contacts</a> </li> </ul> </div> </nav> </header> <section class="forJS text-center">Some info 1</section> <section class="forJS text-center">Some info 2</section> <section class="forJS text-center">Some info 3</section> <section class="text-center">Some info 4</section> <section class="text-center">Some info 5</section> <section class="text-center">Some info 6</section> <section class="text-center">Some info 7</section> <section class="text-center">Some info 8</section> <section class="text-center">Some info 9</section> <section class="forJS text-center">Some info 10</section> </body>
P.S.Look at the last line, there is changeLinkState
. Should it be without parentheses ()
?
And inside while
is empty, why?
Advertisement
Answer
The most minimal change you can make using the current design to implement the desired functionality is testing the section’s height to ensure it’s visible instead of unconditionally adding the active class to the nearest navigation link as in the current code.
if (window.scrollY - sections[index].offsetHeight < sections[index].offsetTop) { links[index].classList.add('active'); }
Instead of:
links[index].classList.add('active');
You can tweak the cutoff point with an offset like scrollY + 50
but hardcoding the number here seems non-ideal.
Full code:
const links = document.querySelectorAll('.nav-link'); const sections = document.querySelectorAll('.forJS'); function changeLinkState() { let index = sections.length; while (--index && window.scrollY + 50 < sections[index].offsetTop) {} links.forEach((link) => link.classList.remove('active')); // add the active class if within visible height of the element if (scrollY - sections[index].offsetHeight < sections[index].offsetTop) { links[index].classList.add('active'); } } changeLinkState(); window.addEventListener('scroll', changeLinkState);
section { height: 100vh; } .nav-link.active { color: red; } section { border: 1px solid #555; }
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/js/bootstrap.min.js"></script> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/css/bootstrap.min.css" rel="stylesheet" /> <body> <header class="fixed-top"> <nav class="navbar navbar-expand-lg navCustom"> <div class="container"> <ul class="navbar-nav justify-content-center"> <li class="nav-item"> <a class="nav-link" href="#main">Main</a> </li> <li class="nav-item"> <a class="nav-link" href="#about">About us</a> </li> <li class="nav-item"> <a class="nav-link" href="#portfolio">Portfolio</a> </li> <li class="nav-item"> <a class="nav-link" href="#contacts">Contacts</a> </li> </ul> </div> </nav> </header> <section class="forJS text-center">Some info 1</section> <section class="forJS text-center">Some info 2</section> <section class="forJS text-center">Some info 3</section> <section class="text-center">Some info 4</section> <section class="text-center">Some info 5</section> <section class="text-center">Some info 6</section> <section class="text-center">Some info 7</section> <section class="text-center">Some info 8</section> <section class="text-center">Some info 9</section> <section class="forJS text-center">Some info 10</section> </body>
Your other questions were addressed in the comments but I’ll reiterate the answers here:
- No parentheses are used on
changeLinkState
because we’re passing the function object itself to the callback to be invoked later. If we invoked it likechangeLinkState()
, we’d wind up passingundefined
into the callback and prematurely firing the handler, as explained here. while
is empty because its block that manipulates the termination condition (i.e.--index
) is merged into the condition as shorthand, as described here.
Beyond that, there are multiple issues with the design I’ll remark on briefly and leave as an exercise to the reader:
- The bootstrap layout spreads the sidebar header across the whole page, so there’s probably unintentional overlap between the header and the element. If the header had a background, content would be occluded. I’d revisit the structure here to ensure multiple, non-overlapping columns or a flow layout was used.
<section>
tags should be in a parent container.- CSS properties shouldn’t be
camelCased
.forJS
is not a particularly clear class name. scroll-y:auto;
is an invalid CSS property. Perhaps you meantoverflow-y: auto;
.- The strategy of firing the scroll event listener and iterating the sections is somewhat primitive. Check out throttling and consider refactoring to use an Intersection Observer.