0

I have a nav which contains letters and sections which are associated to letters in the nav.

When a user scrolls to a section, I want to addClass active to that letter. For example:

  • User scrolls to section with the id of a, the anchor with the data-letter with a will be active.

Currently, on scroll, all my letters in the nav become active and this is because it's always thinking it's on section A.

Demo:

$(function() {
  $(window).scroll(function() {
    
    // step 1: get id of section
    var visible_section = $('section:visible'), id = visible_section.attr('id');
    console.log(id);

    // step 2: add class where id and data-letter match
  $("nav a").removeClass("active");
  $("nav a[data-letter='"+id+"']").addClass("active");
    
  });
});
.nav {
  background: grey;
  padding: 30px 15px;
  position: fixed;
  top: 0;
  width: 100%;
}

.nav a {
  padding: 0 15px;
  text-decoration: none;
}

.nav a:hover {
  background: black;
  color: white;
}

.nav a.active {
  background: black;
  color: white;
}

.sections {
  margin-top: 100px;
}

section {
  padding: 200px 0;
  color: red;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 50px;
}

section:nth-child(odd) {
  background-color: black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js" integrity="sha512-894YE6QWD5I59HgZOGReFYm4dnWc1Qt5NtvYSaNcOP+u1T9qYdvdihz0PPSiiqn/+/3e7Jo4EaG7TubfWGUrMQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>


<main>

  <nav class="nav">
    <a href="#a" data-letter="a">A</a>
    <a href="#b" data-letter="b">B</a>
    <a href="#c" data-letter="c">C</a>
    <a href="#d" data-letter="d">D</a>
  </nav>

  <div class="sections">
    <section id="a">A</section>
    <section id="b">B</section>
    <section id="c">C</section>
    <section id="d">D</section>
  </div>

</main>
Freddy
  • 683
  • 4
  • 35
  • 114

4 Answers4

1

FYI: https://stackoverflow.com/a/5354536/4571790

function isVisible(elm) {
  var rect = elm.getBoundingClientRect();
  var viewHeight = Math.max(document.documentElement.clientHeight, window.innerHeight);
  return !(rect.bottom < 0 || rect.top - viewHeight >= 0);
}

$(function() {
  $(window).scroll(function() {
    
    // step 1: get id of section
    var visible_section = $('section:visible'), id="";
    
    // this code will find which section is the first visible
    $(".sections").find("section").each((i,a)=>id==""?(isVisible(a)?id=$(a).attr("id"):id):id);
    $("#result").html(id +" is visible now");
    //console.log(id);

    // step 2: add class where id and data-letter match
  $("nav a").removeClass("active");
  $("nav a[data-letter='"+id+"']").addClass("active");
    
  });
});
.nav {
  background: grey;
  padding: 30px 15px;
  position: fixed;
  top: 0;
  width: 100%;
}

.nav a {
  padding: 0 15px;
  text-decoration: none;
}

.nav a:hover {
  background: black;
  color: white;
}

.nav a.active {
  background: black;
  color: white;
}

.sections {
  margin-top: 100px;
}

section {
  padding: 200px 0;
  color: red;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 50px;
}

section:nth-child(odd) {
  background-color: black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js" integrity="sha512-894YE6QWD5I59HgZOGReFYm4dnWc1Qt5NtvYSaNcOP+u1T9qYdvdihz0PPSiiqn/+/3e7Jo4EaG7TubfWGUrMQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>


<main>

  <nav class="nav">
    <a href="#a" data-letter="a">A</a>
    <a href="#b" data-letter="b">B</a>
    <a href="#c" data-letter="c">C</a>
    <a href="#d" data-letter="d">D</a>
    <span id="result"></span>
  </nav>

  <div class="sections">
    <section id="a">A</section>
    <section id="b">B</section>
    <section id="c">C</section>
    <section id="d">D</section>
  </div>

</main>
Serhat MERCAN
  • 1,078
  • 3
  • 14
  • 31
0

You should differentiate the nav tabs by setting their data-letter as you described to a, b, c and d:

<nav class="nav">
    <a href="#a" data-letter="a">A</a>
    <a href="#b" data-letter="b">B</a>
    <a href="#c" data-letter="c">C</a>
    <a href="#d" data-letter="d">D</a>
</nav>

All your nav become active not because it always thinks it's on a, but because all data-letter are set to a.

pid
  • 11,472
  • 6
  • 34
  • 63
  • Apologies, that was an oversight my end in the demo. My tabs are differentiated. I have updated my question to correct (and you can run the demo to see the issue). – Freddy Mar 22 '22 at 15:02
0

You can change ratio from 0.1 to 1:

            $(function () {
                let ratio = 0.6; // From 0.1 to 1
                $(window).scroll(function () {
                    let sections = $('.sections section');
                    let scrollTop = $(window).scrollTop();
                    let screen_height = $(window).height();
                    $.each(sections, function () {
                        let top = $(this).offset().top;
                        let calc = (top - scrollTop) / screen_height;
                        if (calc >= (ratio * -1) && calc <= ratio) {
                            let id = $(this).attr('id');
                            $("nav a").removeClass("active");
                            $("nav a[data-letter='" + id + "']").addClass("active");
                            //console.log(`${id} is Active`);
                        }
                    });
                });
            });
.nav {
            background: grey;
            padding: 30px 15px;
            position: fixed;
            top: 0;
            width: 100%;
        }

        .nav a {
            padding: 0 15px;
            text-decoration: none;
        }

        .nav a:hover {
            background: black;
            color: white;
        }

        .nav a.active {
            background: black;
            color: white;
        }

        .sections {
            margin-top: 100px;
        }

        section {
            padding: 200px 0;
            color: red;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 50px;
        }

        section:nth-child(odd) {
            background-color: black;
        }
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<main>

        <nav class="nav">
            <a href="#a" data-letter="a">A</a>
            <a href="#b" data-letter="b">B</a>
            <a href="#c" data-letter="c">C</a>
            <a href="#d" data-letter="d">D</a>
        </nav>

        <div class="sections">
            <section id="a">A</section>
            <section id="b">B</section>
            <section id="c">C</section>
            <section id="d">D</section>
        </div>
    </main>
Younes Bennour
  • 749
  • 1
  • 5
  • 8
0

Your confusion seems to stem from the fact that you have an inaccurate understanding of what :visible selector in jQuery selects. From documentation: "Elements are considered visible if they consume space in the document. Visible elements have a width or height that is greater than zero." By this definition, all of the sections are "visible".

To achieve your use case you need to calculate coordinates of individual sections and compare them with scroll position in the document. The following appears to do what you are looking for (try it out here):

$(function () {
    $(window).scroll(function () {
        // determine element that is fully in the viewport
        const fullyVisibleSection = Array.from(document.querySelectorAll('section')).find((section) => {
            const topY = section.offsetTop;
            const bottomY = topY + section.offsetHeight;
            return (topY >= window.scrollY) && (bottomY <= (window.scrollY + window.innerHeight));
        });
        // only update classes if the section is fully visible
        if (fullyVisibleSection) {
            $("nav a").removeClass("active");
            // add class where id and data-letter match
            $("nav a[data-letter='" + fullyVisibleSection.id + "']").addClass("active");
        }
    });
}); 
Aziz Yokubjonov
  • 656
  • 6
  • 9