Search code examples
javascriptjqueryviewport

Check if element is visible in viewport


I have tried multiple ways to do this now, I am 80% there but it's still not quite there. I have sections of different sizes. And my navigation where I am trying to add an active class when I'm on the corresponding section, showing that it is active.

The code has a tendency to be buggy, inside nav-gallery, I have a masonry gallery, and slick slider I'm not sure if that affects it. But the active class has a tendency to stick on the gallery, in the navigation. Each section is at least 100vh in height.

Per a suggestion from Cedric, I tried with intersection observer API. But it has the same problem, at least in my implementation, it is buggy and gallery somehow is active even when not in the viewport.

  let options = {
    root: null,
    rootMargin: '0px',
    threshold: 1
  }
  let callback = (entries, observer) => {
      console.log("callback called");
    entries.forEach(entry => {
        console.log("set active for " + entry.target.id);
        let sectionId = entry.target.id;
        navItems.each(function(){
            $(this).removeClass('active');
        });
        $("a[href='#" + sectionId + "']").addClass('active');
    });
  };

  let observer = new IntersectionObserver(callback, options);

  pageSections.each(function () {
    let target = document.querySelector("#" + $(this).attr('id'));
    observer.observe(target);
  });

const navItems = $(".navigation-item");
const pageSections = $(".section-page");

const elementIsInView = el => {
    const scroll = window.scrollY || window.pageYOffset
    const boundsTop = el.getBoundingClientRect().top + scroll

    const viewport = {
        top: scroll,
        bottom: scroll + window.innerHeight,
    }

    const bounds = {
        top: boundsTop,
        bottom: boundsTop + el.clientHeight,
    }

    return ( bounds.bottom >= viewport.top && bounds.bottom <= viewport.bottom ) 
        || ( bounds.top <= viewport.bottom && bounds.top >= viewport.top );
}
$(function () {
    $(window).scroll(function () {
        pageSections.each(function () {
            console.log("elements to check " + $(this).attr('id'))
            if(elementIsInView($(this)[0])){
                console.log("element is in viewport " + $(this).attr('id'))
                // this = the section that is visible
                let sectionId = $(this).attr('id');
                navItems.each(function(){
                    $(this).removeClass('active');
                });
                $("a[href='#" + sectionId + "']").addClass('active');
            }
        })
    })
})   
.section-page{
  height:100vh;
}
.navigation-fixed{
  position:fixed;
  top:20px;
  left:20px;
}
.navigation-fixed ul li a.active{
  color:red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<nav class="navigation-fixed">
        <ul>
            <li>
                <a href="#nav-contact" class="navigation-item">CONTACT</a>
            </li>
            <li>
                <a href="#nav-info" class="navigation-item">INFO</a>
            </li>
            <li>
                <a href="#nav-gallery" class="naviation-item">GALLERY</a>
            </li>
            <li>
                <a href="#nav-home" class="navigation-item active">Home</a>
            </li>
        </ul>
    </nav>
    
    <section class="section-page" id="nav-gallery">Content here</section>
<section class="section-page" id="nav-info">Content here</section>
<section class="section-page" id="nav-contact">Content here</section>


Solution

  • Here is a small / basic example using Intersection Observer I hope this will help

    // list of elements to be observed
    const targets = document.getElementsByClassName('section-page')
    
    const options = {
      root: null, // null means root is viewport
      rootMargin: '0px',
      threshold: 0.5 // trigger callback when 50% of the element is visible
    }
    
    
    function callback(entries, observer) { 
      entries.forEach(entry => {
    
        if(entry.isIntersecting){
        	document.querySelector('.active').classList.remove('active');
          const sectionId = entry.target.id; // identify which element is visible in the viewport at 50%
          document.querySelector(`[href="#${sectionId}"]`).classList.add('active');
          
        }
      });
    };
    
    let observer = new IntersectionObserver(callback, options);
    
    [...targets].forEach(target => observer.observe(target));
    body {
      margin: 0;
    }
    
    main {
      margin: 0;
      display: grid;
      grid-template-columns: 150px 1fr;
      font-family: sans-serif; 
    }
    
    #nav {
        list-style-type: none;
    }
    
    .navigation-item {
        color: inherit;
        text-decoration: none;
        display: block;
        margin-bottom: 12px;
        padding: 5px;
    }
    
    .navigation-item.active {
      background-color: grey;
      color: white;
      font-weight: bold;
    }
    
    
    #sections {
      height: 100vh;
      overflow: auto;
    }
    
    .section-page {
      margin: 20px;
      height: 100vh;
      box-sizing: border-box;
      padding: 0 20px 20px;
    }
    
    .section-page:nth-child(1) {
      background-color: crimson;
    }
    
    .section-page:nth-child(2) {
      background-color: darkgreen;
    }
    
    .section-page:nth-child(3) {
      background-color: darkorange;
    }
    
    .section-page:nth-child(4) {
      background-color: darkblue;
    }
    
    .content {
      padding-top: 20px;
      color: white;
      position: sticky;
      top: 0;
    }
    <main>
      <nav>
        <ul id="nav">
          <li>
            <a href="#nav-home" class="navigation-item active">HOME</a>
          </li>
          <li>
            <a href="#nav-gallery" class="navigation-item">GALLERY</a>
          </li>
          <li>
            <a href="#nav-info" class="navigation-item">INFO</a>
          </li>
          <li>
            <a href="#nav-contact" class="navigation-item">CONTACT</a>
          </li>
          
        </ul>
      </nav>
      <div id="sections">
        <section class="section-page" id="nav-home">
          <div class="content">Home section</div>
        </section>
        <section class="section-page" id="nav-gallery">
          <div class="content">Gallery section</div>
        </section>
        <section class="section-page" id="nav-info">
          <div class="content">Info section</div>
        </section>
        <section class="section-page" id="nav-contact">
          <div class="content">Contact section</div>
        </section>
      </div>
    </main>