Search code examples
javascriptscrollobservablemutation-observersintersection-observer

Looking for a modern(akin to an "Observer", without the "scroll" event) way to add body class based on scroll direction


The end goal is to add a scrolling-up class to the body element when the user is scrolling up the page and remove said class when scrolling down.

I am achieving this the old school way, by attaching a throttled(via lodash) callback to the scroll event, like so:

var lastScrollTop = 0;

var checkScrollDirection = function() {
    var currentOffset = window.pageYOffset;
    document.body.classList.toggle('scrolling-up', currentOffset < lastScrollTop );
    lastScrollTop = currentOffset;
};

window.addEventListener('scroll', _.throttle(
        checkScrollDirection,
        100,
        {
            'leading': true,
            'trailing': true
        }
    )
);

This works (very) well, but I am wondering if it is possible to achieve this by using a modern Observer, and therefore taking all this off the main thread. Even if throttled, the above logic still takes alot more CPU time than makes sense from a purely logical standpoint.

Thank you!


Solution

  • If we place some target elements strategically in the body we can have them observed by an IntersectionObserver to sense whether the body is being scrolled up or down.

    The minimum number of such targets would seem to be one viewport height one every other 100vh going down the body (with an adjustment for a remaining bit at the end). This way there is always one, but only one, target in the viewport at any one time. This target can be observed so it triggers code at various thresholds.

    There is a balance to be struck between the number of 'stops' in the viewport and the time taken by observing. This snippet has observation every 5% of the viewport and on the devices tried (laptop, iPad) this seems to give no perceptible problem in practice.

    This method has a slightly hacky feel to it as it entails adding elements to the document, and on a resize we have to resort to JS to recalculate their number, height and position.

    However, the method seems to work, the absolute maximum GPU usage I saw with manic continuous scrolling and change of direction was around 7%. 'Ordinary' sort of scrolling hardly registers. The targets in the snippet have width 1px, I do not know whether it's better or worse processor-usage wise to have them wide or thin. They are placed in the center of the viewport just in case there are any problems with observing right at the edges.

    This snippet just adds class scrolling-up to the body as appropriate and this shows/hides a fixed header.

    //Set up action on intersection being observed
    let previousTarget, previousTop, scrollingUp;
    const header = document.querySelector('.header');
    function scrolledFn(entries) {
      entries.forEach(entry => {
        if (entry.isIntersecting) { //note, only one can intersect at a time
          if (entry.target == previousTarget) {
            const newScrolling = (entry.boundingClientRect.top > previousTop);
            if (newScrolling != scrollingUp) {
              document.body.classList.toggle('scrolling-up');
              scrollingUp = !scrollingUp;
            }
          }
          else {
            previousTarget = entry.target;
          }
          previousTop = entry.boundingClientRect.top;
        }
      });
    }
    
    const step = 0.05;
    let thresholds =[];
    for ( let t = 0; t <= 1; t += step) { thresholds.push(t); }
    let observer = new IntersectionObserver(scrolledFn, {threshold: thresholds});
    
    function setupTargets() {
    
    // first remove any targets we may have already
    const oldTargets = document.querySelectorAll('.observed');
      oldTargets.forEach(oldTarget => { oldTarget.remove();
    });
    
    // Insert targets into the document
    const numWholeViewports = Math.floor(document.body.offsetHeight/window.innerHeight);
    const numFullHeightTargets = Math.floor((numWholeViewports + 1) / 2);
    
    let i = 0;
    function createTarget(h) {
      const el = document.createElement('div');
      document.body.appendChild(el);
      observer.observe(el);
      el.classList.add('observed');
      el.style.top = i*200 + 'vh';
      el.style.height = h + 'vh';
    }
    for (i; i < numFullHeightTargets ; i++) {
      createTarget('100');
    }
    if (numWholeViewports%2 == 0) { createTarget((document.body.offsetHeight%window.innerHeight) * 100 / window.innerHeight); }
    
    previousTop = 0;
    scrollingUp = document.body.scrollTop == 0;
    if (scrollingUp) document.body.classList.add('scrolling-up');
    else document.body.classList.remove('scrolling-up');
    }
    
    window.onload = setupTargets;
    window.onresize = setupTargets;
    body {
      position: relative;
      width: 100vw;
      height: auto;
      overflow-y: auto;
    }
    
    .header {
      display: none;
      position: fixed;
      top: 0px;
      left: 0px;
      background-color: lime;
      width: 100%;
      height: 10%;
      align-items: center;
      justify-content: center;
      font-size: 4rem;
      z-index: 1;
    }
    
    .scrolling-up .header {
      display: flex;
    }
    
    .content {
      position: relative;
      width: 80vw;
      margin: 0 auto;
      top: 10%;
      padding: 10px 20px;
      font-size: 3rem;
    }
    
    .observed {
      width: 1px;
      position: absolute;
      left: 50%;
      margin: 0;
      padding: 0;
      border-width: 0;
      z-index: -99999;
    }
    <body>
    <header class="header">HEADER</header>
    <div class="content">
    <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam porttitor erat ut libero molestie, sit amet facilisis libero finibus. Vestibulum tincidunt, augue faucibus faucibus mattis, quam magna sollicitudin ante, sed hendrerit nisl dolor sit amet neque. Integer feugiat malesuada lobortis. Quisque accumsan, nulla efficitur sodales eleifend, purus arcu lacinia nunc, id consequat mauris nisi ultricies lacus. Cras finibus commodo ipsum, in dictum mauris molestie in. Maecenas pretium ipsum ac velit porttitor, eu sagittis sapien vehicula. Donec vulputate urna non dui egestas iaculis.
    </p><p>
    Etiam sit amet eros in purus venenatis tristique. Donec vel tortor facilisis, tempus nibh a, consectetur velit. Aenean suscipit lacus diam, et ultricies nunc interdum quis. Nam orci sem, hendrerit sit amet lectus nec, convallis facilisis erat. Duis blandit nibh neque, quis porta mauris consectetur sit amet. Nam porttitor dolor vel euismod porttitor. Cras commodo tristique nunc. Proin ultrices sed odio et elementum. Praesent ex dui, placerat sed libero sed, consectetur pellentesque erat. Quisque volutpat molestie nisi eget tristique.
    </p><p>
    Nam sapien mi, mollis eu scelerisque sit amet, tristique eu purus. In quis feugiat massa. Suspendisse ac tellus neque. Vivamus risus nisl, posuere id sem id, aliquet semper nibh. Sed elementum facilisis bibendum. Maecenas ac nunc placerat lectus ultrices sodales. Nunc nec augue purus. Vestibulum a molestie lacus.
    </p><p>
    Curabitur ut tortor dolor. Suspendisse semper, leo et luctus laoreet, odio magna sagittis elit, sed bibendum risus lacus ut metus. Nulla a lobortis massa. Pellentesque volutpat iaculis faucibus. Integer vel erat sed orci lobortis rhoncus ornare ut purus. Phasellus rutrum varius rutrum. Curabitur fermentum finibus tortor at placerat. Pellentesque cursus nibh in dolor dictum tristique. Fusce auctor sapien libero, et porta sem pulvinar eu. Praesent lobortis lacus eget lacus fringilla posuere. Mauris vehicula tortor ut elit tincidunt luctus.
    </p><p>
    Pellentesque porttitor id nulla vitae auctor. Nam orci urna, molestie nec lorem sed, porttitor pulvinar erat. Proin magna sapien, molestie a ipsum eget, iaculis ornare ipsum. Cras imperdiet purus sed sapien sodales sodales. Nam dui nulla, ornare id ornare vel, placerat scelerisque velit. Nunc non dignissim orci. Aliquam diam massa, hendrerit at consectetur eu, eleifend vestibulum erat. Fusce tincidunt eget dolor at faucibus. Donec euismod elementum tellus, eu tristique massa malesuada vel. Aenean sit amet enim id elit sollicitudin dictum.
    </p>
    <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam porttitor erat ut libero molestie, sit amet facilisis libero finibus. Vestibulum tincidunt, augue faucibus faucibus mattis, quam magna sollicitudin ante, sed hendrerit nisl dolor sit amet neque. Integer feugiat malesuada lobortis. Quisque accumsan, nulla efficitur sodales eleifend, purus arcu lacinia nunc, id consequat mauris nisi ultricies lacus. Cras finibus commodo ipsum, in dictum mauris molestie in. Maecenas pretium ipsum ac velit porttitor, eu sagittis sapien vehicula. Donec vulputate urna non dui egestas iaculis.
    </p>
    </div>

    Note, on IOS there may be a slight 'edge' case on scrolling back up to the very top - needs investigating - may be related to 100vh not being the viewport height when there are navbars at the top in the browser.