5

This script issues the active class for the active section. Recently noticed that it stops working on small screens. Even in the developer's console in chrome, I will start to increase the screen size and it will appear, as soon as I start to reduce it immediately stops working (the active class disappears). But only for one long section, in the shorter ones everything works. How can this be fixed?

In the snippet, I set a large fixed height, so the portfolio link does not receive the active class, in my example, when the section width increases, its height decreases, so at some point everything starts working.

const links = document.querySelectorAll('.nav-link');
const sections = [... document.querySelectorAll('.forJS')];

const callback = (entries) => {
  links.forEach((link) => link.classList.remove('active'));
  const elem = entries.find((entry) => entry.isIntersecting);
  if (elem) {
    const index = sections.findIndex((section) => section === elem.target);
    links[index].classList.add('active');
  }
}

let observer = new IntersectionObserver(callback, {
  rootMargin: '0px',
  threshold: 0.5
});

sections.forEach((section) => observer.observe(section));
section {
  height: 100vh;
  scroll-y: auto;
}
.long{
height: 300vh;
}
.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 long">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>

1 Answers1

6

The main issue is threshold: 0.5. This tells the observer to fire once 50% of the element is visible in the viewport. For your "long" element, since its 300vh tall, and your viewport is 100vh tall, the maximum visibility that it has is 100vh/300vh = 33%, so the observer never fires.

To deal with this, you could adjust the threshold to something smaller like 0.25. That would fix the behavior for the long section, but it would make the active link change early for your shorter sections. So I propose you add 2 observers: 1 for the short sections with a threshold of 0.5 (.forJS:not(.long)), and another for the longer sections with a threshold of 0.25 (.forJS.long).

const links = document.querySelectorAll('.nav-link');
const sectionsShort = [...document.querySelectorAll('.forJS:not(.long)')];
const sectionsLong = [...document.querySelectorAll('.forJS.long')];
const sections = [...document.querySelectorAll('.forJS')];

const callback = entries => {
    links.forEach((link) => link.classList.remove('active'));
    const elem = entries.find((entry) => entry.isIntersecting);
    if (elem) {
        const index = sections.findIndex((section) => section === elem.target);
        links[index].classList.add('active');
    }
}

const observerShort = new IntersectionObserver(callback, {
    rootMargin: '0px',
    threshold: .5,
});
const observerLong = new IntersectionObserver(callback, {
    rootMargin: '0px',
    threshold: .25,
});
sectionsShort.forEach((section) => {
    observerShort.observe(section)
});
sectionsLong.forEach((section) => {
    observerLong.observe(section)
});
Glorfindel
  • 20,880
  • 13
  • 75
  • 99
chiliNUT
  • 17,890
  • 12
  • 63
  • 99
  • Thank you for the answer! There is a simpler script without using InsertsectionObserver that does all the same. But the bottom line is that when the user is, for example, not in the "portfolio" section, then the portfolio link should not be active, right? In your case, always the active link of the previous active section. –  Feb 20 '21 at 22:01
  • This script does exactly what your example does: https://stackoverflow.com/questions/66166602/add-class-active-on-scroll-vanilla-js –  Feb 20 '21 at 22:04
  • @vcxbgfx I wasn't sure if that was the intended behavior or not. See the update to the answer, the active links stays as it was – chiliNUT Feb 20 '21 at 22:04
  • @vcxbgfx it does it in a different way. Using an `IntersectionObserver` is a better way to do this than the other answer you linked, because it was specifically designed for this use case, whereas the scroll event needs to listen for any scroll on the page and involves manually inspecting scroll offsets and element offsets – chiliNUT Feb 20 '21 at 22:06
  • Everything works well, but there is a slight conflict in these two pieces of code. After reloading the page or updating it, the active class disappears, even if we are on the active zone. If you remove one of them, everything works fine. Maybe some kind of check is needed before executing them? `sectionsShort.forEach((section) => {observerShort.observe(section)}); sectionsLong.forEach((section) => {observerLong.observe(section)});` –  Feb 21 '21 at 03:58
  • Yeah I dunno, I think its related to this other question which doesn't even have an answer: https://stackoverflow.com/questions/57946043/trigger-intersectionobserver-when-element-is-already-in-viewport – chiliNUT Feb 21 '21 at 04:12