Reputation: 427
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/[email protected]/dist/js/bootstrap.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/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?
Upvotes: 2
Views: 2812
Reputation: 56885
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/[email protected]/dist/js/bootstrap.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/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:
changeLinkState
because we're passing the function object itself to the callback to be invoked later. If we invoked it like changeLinkState()
, we'd wind up passing undefined
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:
<section>
tags should be in a parent container.camelCased
. forJS
is not a particularly clear class name.scroll-y:auto;
is an invalid CSS property. Perhaps you meant overflow-y: auto;
.Upvotes: 1