6

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>

2 Answers2

10

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
  • 21,988
  • 13
  • 81
  • 109
chiliNUT
  • 18,989
  • 14
  • 66
  • 106
  • 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
0

Please change the threshold like in below,it will work.

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:[.5,0] // threshold changed
});

sections.forEach((section) => observer.observe(section));
rocky
  • 23
  • 6
  • This is a copy of the above answer, where only the value for `threshold` was changed. If the value in the initial answer is not working, you should comment there instead of creating a new answer, or at least point out the difference and why the change is necessary. – Moritz Ringler Feb 17 '23 at 12:47
  • This does not provide an answer to the question. Once you have sufficient [reputation](https://stackoverflow.com/help/whats-reputation) you will be able to [comment on any post](https://stackoverflow.com/help/privileges/comment); instead, [provide answers that don't require clarification from the asker](https://meta.stackexchange.com/questions/214173/why-do-i-need-50-reputation-to-comment-what-can-i-do-instead). - [From Review](/review/late-answers/33843514) – Moritz Ringler Feb 17 '23 at 12:48
  • Then you could have provided some answer to the question,it worked for me that is why i shared it,i dont see any problem here. – rocky Feb 27 '23 at 06:42