6

EDIT On closer inspection this is pretty broken when resizing the viewport. It stops working or on narrow windows and the scroll is super super fast. So I'm putting it up for bounty!

--

I've seen something similar and have been trying to refactor the JS. I have two columns of content, which when scrolled move in opposite directions. This should loop continuously.

The issue is if I remove the height from the .project element. The content will scroll smoothly when scrolling down but not up. The height/length of content will vary so I can't really have a fixed value here.

This seems to depends on viewport height. If the UI behaves as intended and I reduce the width of the viewport it can stop working like described above. But if I then reduce the height - it can begin to behave correctly again. So maybe it's down to how much content is visible in the viewport on load?

Example (also in code snippet): https://jsfiddle.net/rdowb0y5/1

I will add a 'media query' so that this is only visible on tablet/desktop views and on mobile devices the JS is removed and the content just stacked.

Thanks in advance - really looking forward to some support on this!

$(document).ready(function() {

    var num_children=$('.split-loop__left').children().length;
    var child_height=$('.split-loop__right').height() / num_children;
    var half_way=num_children * child_height / 2;
    $(window).scrollTop(half_way);

    function crisscross() {

      var parent=$(".split-loop"); //.first();
      var clone=$(parent).clone();

      var leftSide=$(clone).find('.split-loop__left');
      var rightSide=$(clone).find('.split-loop__right');

      if (window.scrollY > half_way) {
        //We are scrolling up
        $(window).scrollTop(half_way - child_height);

        var firstLeft=$(leftSide).children().first();
        var lastRight=$(rightSide).children().last();

        lastRight.appendTo(leftSide);
        firstLeft.prependTo(rightSide);

      }

      else if (window.scrollY < half_way - child_height) {

        var lastLeft=$(leftSide).children().last();
        var firstRight=$(rightSide).children().first();

        $(window).scrollTop(half_way);
        lastLeft.appendTo(rightSide);
        firstRight.prependTo(leftSide);
      }

      $(leftSide).css('bottom', '-'+ window.scrollY + 'px');
      $(rightSide).css('bottom', '-'+ window.scrollY + 'px');

      $(parent).replaceWith(clone);
    }

    $(window).scroll(crisscross);

  }

);
/* Hide Scroll Bars */

::-webkit-scrollbar {
  display: none;
}

html,
body {
  margin: 0;
  padding: 0;
  -ms-overflow-style: none;
  /* IE and Edge */
  scrollbar-width: none;
  /* Firefox */
}


/* Basic Styling */

img {
  border: 1px solid black;
  margin-bottom: 24px;
  width: 100%;
  max-width: 100%;
}

h2 {
  font-size: 14px;
  font-weight: normal;
  margin-bottom: 4px;
  font-family: 'Inter', sans-serif;
}

p {
  color: black;
  font-size: 11px;
  font-family: 'Inter', sans-serif;
}


/* Content will be in these eventually */

.bar-left,
.bar-right {
  border-right: 1px solid black;
  box-sizing: border-box;
  height: 100vh;
  position: fixed;
  top: 0;
  left: 0;
  width: 48px;
  z-index: 10000;
}

.bar-right {
  border: none;
  border-left: 1px solid black;
  left: auto;
  right: 0;
}


/* Split Loop */

.split-loop {
  position: relative;
  margin: 0 48px;
}

.split-loop__left {
  // position: absolute;
  // left: 0%;
  // top: 0%;
  // right: auto;
  // bottom: auto;
  // z-index: 4;
  width: 50%;
}

.split-loop__right {
  border-left: 1px solid black;
  box-sizing: border-box;
  position: fixed;
  right: 48px;
  bottom: 0;
  z-index: 5;
  width: calc(50% - 48px);
}

.project {
  box-sizing: border-box;
  border-bottom: 1px solid black;
  height: 600px;
  padding: 48px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.0/jquery.min.js"></script>

<header class="bar-left">

</header>

<div class="bar-right">

</div>

<div class="view">

  <div class="grid split-loop">

    <div class="split-loop__left">

      <div class="grid__item project">
        <img src="https://www.fillmurray.com/600/400" alt="" class="project__media" />
        <h2 class="project__title">Project Title</h2>
        <p class="project__desc">Short Description</p>
      </div>

      <div class="grid__item project">
        <img src="https://www.fillmurray.com/g/600/400" alt="" class="project__media" />
        <h2 class="project__title">Project Title</h2>
        <p class="project__desc">Short Description</p>
      </div>

      <div class="grid__item project">
        <img src="https://www.fillmurray.com/600/400" alt="" class="project__media" />
        <h2 class="project__title">Project Title</h2>
        <p class="project__desc">Short Description</p>
      </div>

    </div>

    <div class="split-loop__right">

      <div class="grid__item project">
        <img src="https://www.fillmurray.com/g/600/400" alt="" class="project__media" />
        <h2 class="project__title">Project Title</h2>
        <p class="project__desc">Short Description</p>
      </div>

      <div class="grid__item project">
        <img src="https://www.fillmurray.com/600/400" alt="" class="project__media" />
        <h2 class="project__title">Project Title</h2>
        <p class="project__desc">Short Description</p>
      </div>

      <div class="grid__item project">
        <img src="https://www.fillmurray.com/g/600/400" alt="" class="project__media" />
        <h2 class="project__title">Project Title</h2>
        <p class="project__desc">Short Description</p>
      </div>

    </div>

  </div>

</div>
user1406440
  • 1,329
  • 2
  • 24
  • 59
  • 2
    I believe [CodeReview](https://codereview.stackexchange.com/questions/tagged/javascript) would possibly be a better place to post this question for three reasons. 1) It's almost working fine 2) Guys there enjoy more spending time to debug interesting concepts. 3) CodeReview is less busy so your question won't get lost from the sight as esaily as it does here. – Redu Dec 12 '21 at 12:37
  • Thanks @Redu! If people think that's best I can do that - or would an admin need to move? Whatever people think is best! :) – user1406440 Dec 12 '21 at 13:09
  • 1
    No big deal just repost there. – Redu Dec 12 '21 at 13:12
  • Thanks - didn't know it existed! – user1406440 Dec 12 '21 at 18:21

2 Answers2

4

Familytype.co has following constrains:

  1. There needs to be one more scrolling container. They are using scrollbar on body.
  2. Both lanes need to be of equal height. Their slides have different heights but they are all multiples of 50vh. And they managed to have same height lanes. Heights are still based on viewports see their .block--50 and .block--100 style rules.
  3. You need to repeat first two items in same lane to see smooth shift to first slide.

In Following, familytype.co style, approach I've repeated first two elements in left lane. And then cloned all elements of left lane to right in javascript. Also added a media query to show only one lane if viewport width gets below 576px.

let view, parent, leftSide, rightSide;

$(document).ready(function() {
  view = $('#view');
  parent = $(".split-loop");
  leftSide = $(parent).find('.split-loop__left');
  rightSide = $(parent).find('.split-loop__right');

  centerScrollPosition();
  window.onresize = function(event) {
    centerScrollPosition();
  };

  //repeat first 2
  $(leftSide).append($($(leftSide).children()[0]).clone());
  $(leftSide).append($($(leftSide).children()[1]).clone());
  //clone left lane to right      
  let arr = [];
  $(leftSide).children().each((index, elm) => {
    arr.push($(elm).clone());
  });

  //shift right lane by half
  //arr.push.apply(arr, arr.splice(0, arr.length / 2));
  //arr.push(arr[0].clone());
  //arr.push(arr[1].clone());
  $(rightSide).append(arr);

  //listen to scroll events
  //incase you want movement to happen on user scroll
  $(view).add(window).scroll(function() {
    //$(view).on('wheel', function (event) {
    handleScroll();
  });
  handleScroll();

});


function centerScrollPosition() {
  //center the content
  var vh = $(window).height();
  var lh = leftSide.height();
  var half_way = (lh - vh) / 2;
  $(window).scrollTop(half_way);
}

function handleScroll() {
  var scroll = $(window).scrollTop();
  var sh = $(leftSide).height() - window.innerHeight;

  if (scroll >= (sh - 1)) {
    $(window).scrollTop(2);
  } else if (scroll == 0) {
    $(window).scrollTop(sh - 1);
  }
  $(rightSide).css({
    "transform": "translate3d(0, " + ((((sh) - scroll * 2) * -1)) + "px, 0)"
  });

}
* {
  box-sizing: border-box;
}

 ::-webkit-scrollbar {
  display: none;
}

html,
body {
  margin: 0;
  padding: 0;
  height: 100%;
  width: 100%;
  background-color: white;
}

body {
  overflow-y: scroll
}


/* Basic Styling */

img {
  max-width: 90%;
  object-fit: fill;
  margin-top: 1rem;
  align-self: center;
  border: 1px solid black;
}

h2 {
  font-size: 14px;
  font-weight: normal;
  margin-bottom: 4px;
  font-family: 'Inter', sans-serif;
  text-align: center;
}

p {
  color: black;
  font-size: 11px;
  font-family: 'Inter', sans-serif;
  text-align: center;
}


/* center line for debugging */

.center {
  position: fixed;
  width: 100vw;
  top: 50%;
  left: 0%;
  z-index: 100000;
  transform: translateY(-50%);
  border-bottom: 2px dashed gray;
}


/* Content will be in these eventually */

.view {
  overflow: hidden;
  position: relative;
  width: 80vw;
  margin: 0 auto;
}

.bar-left,
.bar-right {
  /*border-right: 1px solid black;*/
  box-sizing: border-box;
  height: 100vh;
  position: fixed;
  top: 0;
  left: 0;
  width: 5vw;
  z-index: 10000;
}

.bar-right {
  border: none;
  /*border-left: 1px solid black;*/
  left: auto;
  right: 0;
}


/* Split Loop */

.split-loop {
  position: relative;
  width: 100%;
  height: 100%;
}

.split-loop>div {
  max-width: 50%;
  float: left;
  height: auto;
  transform-style: preserve-3d;
  display: flex;
  flex-flow: column;
}

.split-loop__right {
  will-change: transform;
  perspective: 1000;
  backface-visibility: hidden;
}

.project {
  border: 1px solid black;
  min-height: auto;
  background-color: lightskyblue;
  display: flex;
  flex-flow: column nowrap;
}

@media(max-width: 576px) {
  .split-loop>div {
    max-width: 100%;
  }
  .split-loop__right {
    display: none;
  }
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<!-- span class="center"></span -->
<header class="bar-left"></header>
<div class="bar-right"></div>

<div id=view class="view">
  <div class="grid split-loop">
    <div class="split-loop__left">
      <div class="grid__item project">
        <img src="https://www.fillmurray.com/600/400" alt="" class="project__media" />
        <h2 class="project__title">Project Title 1</h2>
        <p class="project__desc">Short Description Lorem ipsum dolor sit amet consectetur adipisicing elit. Consequuntur, id?Short Description Lorem ipsum dolor sit amet consectetur adipisicing elit. Consequuntur, id?</p>
      </div>

      <div class="grid__item project">
        <img src="https://www.placecage.com/c/460/300" alt="" class="project__media" />
        <h2 class="project__title">Project Title 2</h2>
        <p class="project__desc">Short Description Lorem ipsum dolor sit amet.</p>
      </div>

      <div class="grid__item project">
        <img src="https://www.placecage.com/g/155/300" alt="" class="project__media" />
        <h2 class="project__title">Project Title 3</h2>
        <p class="project__desc">Short Description Lorem ipsum dolor sit amet consectetur, adipisicing elit. Aspernatur nostrum in obcaecati itaque explicabo voluptatibus corporis cumque praesentium eaque beatae!</p>
      </div>
      <div class="grid__item project">
        <img src="https://www.placecage.com/140/100" alt="" class="project__media" />
        <h2 class="project__title">Project Title 4</h2>
        <p class="project__desc">Short Description Lorem, ipsum dolor sit amet consectetur adipisicing elit. Dolores nobis eius, minus optio ab earum. Lorem ipsum dolor sit amet consectetur adipisicing elit. Architecto, at. Fuga nisi nulla laborum explicabo possimus repellendus
          amet quidem eos.</p>
      </div>

      <div class="grid__item project">
        <img src="https://www.stevensegallery.com/460/300" alt="" class="project__media" />
        <h2 class="project__title">Project Title 5</h2>
        <p class="project__desc">Short Description Lorem ipsum dolor sit amet consectetur adipisicing elit. Nihil?</p>
      </div>

      <div class="grid__item project">
        <img src="https://www.fillmurray.com/g/600/400" alt="" class="project__media" />
        <h2 class="project__title">Project Title 6</h2>
        <p class="project__desc">Short Description Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quia iste distinctio doloremque facere!</p>
      </div>
    </div>

    <div class="split-loop__right">

    </div>
  </div>
</div>

More clever placement is needed in second lane.

Old Answer: Instead of using scrollbar we can do it with transform. Explanation is in the code:

    //variables to track left and right container scrolls
    var leftX = 0, rightX = 0;
    //how quickly shifts will happen
    //depends on how many images are in a column
    //range 1.0 - 5.0
    var shiftingThreshold = 1.7;
    var isAnimating = false;
    var parent, leftSide, rightSide;


    $(document).ready(function () {
      parent = $(".split-loop");
      leftSide = $(parent).find('.split-loop__left');
      rightSide = $(parent).find('.split-loop__right');

      centerScrollPosition();
      window.onresize = function (event) {
        centerScrollPosition();
      };

      document.querySelector('.split-loop__left').addEventListener('transitionend', () => {
        isAnimating = false;
        //positive value means clockwise
        //negative means counter clockwise
        //remove if you don't want to run loop
        //handleScroll(-125);
      });

      //listen to scroll events
      //incase you want movement to happen on user scroll
      $('#view').on('wheel', function (event) {
        handleScroll(event.originalEvent.deltaY);
      });

      //initiate continuous loop
      setTimeout(function () {
        //handleScroll(-125);
      }, 3000);
    });

    function centerScrollPosition() {
      //center the content
      var vh = $('#view').height();
      var slh = $('.split-loop__left').height();
      var half_way = (slh - vh) / 2;
      $('#view').scrollTop(half_way);
    }

    function handleScroll(deltaY) {
      //handle next scroll event only after last transition ends.
      if (isAnimating) {
        return;
      }
      isAnimating = true;
      var isUpShifted = true;
      var isJumped = true;

      leftX -= deltaY;
      rightX += deltaY;

      if (deltaY !== 0) {
        var childHeight = $('.split-loop__left').children().first().height();
        if ((leftX + childHeight / shiftingThreshold) <= 0) { //clockwise shift
          isUpShifted = true;
          var firstLeft = $(leftSide).children().first();
          var lastRight = $(rightSide).children().last();

          //shift left-first to right
          firstLeft.prependTo(rightSide);

          //shift right-last to left
          lastRight.appendTo(leftSide);

          leftX += childHeight + deltaY;
          rightX -= childHeight + deltaY;

        } else if ((leftX - childHeight / shiftingThreshold) > 0) { //anti clockwise shift

          isUpShifted = false;
          var lastLeft = $(leftSide).children().last();
          var firstRight = $(rightSide).children().first();

          //shift right-first to left
          firstRight.prependTo(leftSide);
          //shift left-last to right
          lastLeft.appendTo(rightSide);

          leftX -= childHeight - deltaY;
          rightX += childHeight - deltaY;
        } else {
          isJumped = false;
        }

        //if we've moved projects to other container then 
        //adjust position before smooth scrolling effect
        if (isJumped) {
          $('.split-loop__left').css('transform', 'translateY(' + leftX + 'px)');
          $('.split-loop__right').css('transform', 'translateY(' + rightX + 'px)');
          $('.split-loop__left').css('transition-duration', '0s');
          $('.split-loop__right').css('transition-duration', '0s');
          if (isUpShifted) {
            leftX -= deltaY;
            rightX += deltaY;
          } else {
            leftX -= deltaY;
            rightX += deltaY;
          }
        }

        //do smooth scroll effect
        setTimeout(function () {
          $('.split-loop__left').css('transition-duration', '1.5s');
          $('.split-loop__right').css('transition-duration', '1.5s');
          $('.split-loop__left').css('transform', 'translateY(' + leftX + 'px)');
          $('.split-loop__right').css('transform', 'translateY(' + rightX + 'px)');
        }, 5);

      }

    }
* {
      box-sizing: border-box;
    }

    html,
    body {
      margin: 0;
      padding: 0;
      height: 100%;
      width: 100%;
      background-color: wheat;
    }


    /* Basic Styling */
    img {
      max-width: 80%;
      aspect-ratio: 6/4;
      object-fit: fill;

      margin-top: 10%;
      align-self: center;
      border: 1px solid black;
    }

    h2 {
      font-size: 14px;
      font-weight: normal;
      margin-bottom: 4px;
      font-family: 'Inter', sans-serif;

      text-align: center;
    }

    p {
      color: black;
      font-size: 11px;
      font-family: 'Inter', sans-serif;
      text-align: center;
    }

    /* center line for debugging */
    .center {
      position: fixed;
      width: 100vw;
      top: 50%;
      left: 0%;
      z-index: 100000;
      transform: translateY(-50%);
      border-bottom: 2px dashed gray;
    }



    /* Content will be in these eventually */
    .view {
      overflow: hidden;
      position: relative;
      width: 80vw;
      height: 100vh;
      margin: 0 auto;
    }

    .bar-left,
    .bar-right {
      /*border-right: 1px solid black;*/
      box-sizing: border-box;
      height: 100vh;
      position: fixed;
      top: 0;
      left: 0;
      width: 5vw;
      z-index: 10000;
    }

    .bar-right {
      border: none;
      /*border-left: 1px solid black;*/
      left: auto;
      right: 0;
    }

    /* Split Loop */
    .split-loop {
      position: relative;
      width: 100%;
      height: 100%;
    }

    .split-loop>div {
      max-width: 50%;
      float: left;
      transition-timing-function: linear;
      height: auto;
      will-change: transform;
    }

    .project {
      border: 1px solid black;
      min-height: 70vh;
      background-color: lightskyblue;

      display: flex;
      flex-flow: column nowrap;
    }
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<!-- span class="center"></span -->
  <header class="bar-left"></header>
  <div class="bar-right"></div>

  <div id=view class="view">
    <div class="grid split-loop">
      <div class="split-loop__left">
        <div class="grid__item project">
          <img src="https://www.fillmurray.com/600/400" alt="" class="project__media" />
          <h2 class="project__title">Project Title 1</h2>
          <p class="project__desc">Short Description Lorem ipsum dolor sit amet consectetur adipisicing elit.
            Consequuntur, id?</p>
        </div>

        <div class="grid__item project">
          <img src="https://www.fillmurray.com/g/600/400" alt="" class="project__media" />
          <h2 class="project__title">Project Title 2</h2>
          <p class="project__desc">Short Description Lorem ipsum dolor sit amet.</p>
        </div>

        <div class="grid__item project">
          <img src="https://www.fillmurray.com/600/400" alt="" class="project__media" />
          <h2 class="project__title">Project Title 3</h2>
          <p class="project__desc">Short Description Lorem ipsum dolor sit amet consectetur, adipisicing elit.
            Aspernatur nostrum in obcaecati itaque explicabo voluptatibus corporis cumque praesentium eaque beatae!</p>
        </div>

      </div>

      <div class="split-loop__right">
        <div class="grid__item project">
          <img src="https://www.fillmurray.com/g/600/400" alt="" class="project__media" />
          <h2 class="project__title">Project Title 4</h2>
          <p class="project__desc">Short Description Lorem, ipsum dolor sit amet consectetur adipisicing elit. Dolores
            nobis eius, minus optio ab earum.</p>
        </div>

        <div class="grid__item project">
          <img src="https://www.fillmurray.com/600/400" alt="" class="project__media" />
          <h2 class="project__title">Project Title 5</h2>
          <p class="project__desc">Short Description Lorem ipsum dolor sit amet consectetur adipisicing elit. Nihil?</p>
        </div>

        <div class="grid__item project">
          <img src="https://www.fillmurray.com/g/600/400" alt="" class="project__media" />
          <h2 class="project__title">Project Title 6</h2>
          <p class="project__desc">Short Description Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quia iste
            distinctio doloremque facere!</p>
        </div>

      </div>
    </div>
  </div>

Three items per column are very few to smoothly shift on small viewports. You need to play with shiftingThreshold.
the Hutt
  • 16,980
  • 2
  • 14
  • 44
  • Thanks but unfortunately I need the content to only move on scroll like the original example. I can't have a `height` set on each time as the content of each block will differ. So the original effect was what I'm after if just doesn't seem to work that well depending on viewport height. Thanks for the solution though, would be great in another circumstance! – user1406440 Dec 18 '21 at 19:32
  • In the title you've used "continuous/loop" words so I thought you want it automatic. Like mentioned in the code you just have to comment two `handleScroll(-125)` lines to make it not scroll automatically. The code already has provision to handle user scroll(mouse wheel). I've updated the answer. The reason we need to keep heights relative to viewport height is that if user zooms out too much then all elements will be visible without scroll. In order to get better idea it would be great if you share the website/codepen you are trying to mimic. – the Hutt Dec 19 '21 at 03:23
  • Thanks again! Example doesn't scroll for me or I need to scroll a lot/very quickly to get it to move? If we need to keep a fixed height height, the original posted example probably works best? But that's what I'm trying to change, so there's no height so the content can be longer/different. Maybe we could limit zoom on the container? My example is taken from this: https://scroll-interactions.webflow.io/split-scrolling-loop but I've also just found this which is very similar to what I'm trying to achieve (just without fixed height): https://familytype.co/#typefaces – user1406440 Dec 19 '21 at 10:49
  • I guess performance-wise i'd be bad - so not a good idea! But wonder if height value could be set in CSS. Be a bit OTT as the layout doesn't need it - but the JS does? – user1406440 Dec 19 '21 at 11:04
  • In your snippet in OP, the project elements change lanes, moving clockwise or counterclockwise, that makes things complicated. But if you want familytype.co style effect then it's simple. There elements don't change lanes. It's like two separate vertical carousels moving in opposite direction. If you want like that then can be done easily. – the Hutt Dec 19 '21 at 12:02
  • I think that would be a solution. I will have enough items/blocks anyways so switching lanes probably isn't needed. So I suppose then on top of the alternative scroll, it would just need to clone in the same lane/column to continuously loop scroll? Do you think that'd help with not having a height set as well? – user1406440 Dec 22 '21 at 10:47
  • 1
    I guess it means theres a problem elsewhere but the height issue looks like it's solved by adding more content. I think this means if too much of the `.project` divs are in view the 'clone' can't do its job? I'm interested in the familytype flow as a comparison. Also wonder if having one column with all the content then duplicating/reversing the order of a 2nd column could be an approach? Removed height + added `matchMedia` to stop JS from running on mobile - though it doesn't seem to trigger when scaling down from desktop at the min: https://codepen.io/moy/pen/jOGGBwe – user1406440 Dec 22 '21 at 21:41
  • Sorry - missed the update! Looks good with the media query as well! What are your thoughts on duplicated content scrolling vs. having unique content in each column? I suppose it's a fun effect so not too important but just wondering what you think works best? Thanks again! – user1406440 Dec 24 '21 at 19:51
  • Duplicate contents is easy to implement but same item will align, in both tracks, at some point while scrolling. For unique content you'll have to merge them in left column on mobiles otherwise user will miss out on content in the right column. – the Hutt Dec 25 '21 at 03:56
3

This example is a bit sloppy and without using jQuery, but it works with two interdependent scrolls (left and right blocks). I hope this will somehow help you in solving your problem.

const rootElement = document.querySelector(".split-loop");
const leftLoop = document.querySelector(".split-loop__left");
const rightLoop = document.querySelector(".split-loop__right");

const scrollHeight = leftLoop.scrollHeight;
const offsetBoundary = 200; //the offset from the borders at which the element reordering event is triggered
const lastScrollPos = new WeakMap();
const linkedLoops = new Map([
  [leftLoop, rightLoop],
  [rightLoop, leftLoop],
]);

let scrollLockElement = null;
let scrollLockTimeout = null;

// the function sets handlers to scrolling for infinite scrolling
function infiniteScrollHandler(loop) {
  const virtualLoop = Array.from(loop.children);
  virtualLoop.forEach(
    (el) => (el.style.top = scrollHeight / 2 + el.offsetHeight + "px")
  );

  loop.addEventListener("scroll", () => {
    if (virtualLoop.length < 2) return; // not enough items to scroll

    const topBound = loop.scrollTop;
    const bottomBound = loop.scrollTop + loop.offsetHeight;
    const firstEl = virtualLoop[0];
    const lastEl = virtualLoop[virtualLoop.length - 1];

    if (firstEl.offsetTop >= topBound - offsetBoundary) {
      lastEl.style.top = firstEl.offsetTop - lastEl.offsetHeight + "px";
      virtualLoop.unshift(lastEl);
      virtualLoop.pop();
    } else if (
      lastEl.offsetTop + lastEl.offsetHeight <
      bottomBound + offsetBoundary
    ) {
      firstEl.style.top = lastEl.offsetTop + lastEl.offsetHeight + "px";
      virtualLoop.push(firstEl);
      virtualLoop.shift();
    }
  });
}

// the function sets handlers to scrolling for reverse interaction with the linked loop
function reverseLinkLoopHandler(loop) {
  loop.addEventListener("scroll", () => {
    const delta = lastScrollPos.get(loop) - loop.scrollTop;
    lastScrollPos.set(loop, loop.scrollTop);
    // this is blocked to prevent deadlock when events of two blocks are called each other.
    {
      if (scrollLockElement !== null && scrollLockElement !== loop)
        return;
      scrollLockElement = loop;
      clearTimeout(scrollLockTimeout);
      scrollLockTimeout = setTimeout(
        () => (scrollLockElement = null),
        300
      );
    }

    linkedLoops
      .get(loop)
      .scrollTo(0, linkedLoops.get(loop).scrollTop + delta);
  });
}

// set scroll handlers on all loops
linkedLoops.forEach((loop) => {
  infiniteScrollHandler(loop);
  loop.scrollTo(0, scrollHeight / 2);
  lastScrollPos.set(loop, scrollHeight / 2);
  reverseLinkLoopHandler(loop);
});
/* Hide Scroll Bars */

 ::-webkit-scrollbar {
  display: none;
}

html,
body {
  margin: 0;
  padding: 0;
  -ms-overflow-style: none;
  /* IE and Edge */
  scrollbar-width: none;
  /* Firefox */
}


/* Basic Styling */

img {
  border: 1px solid black;
  margin-bottom: 24px;
  width: 100%;
  max-width: 100%;
}

h2 {
  font-size: 14px;
  font-weight: normal;
  margin-bottom: 4px;
  font-family: "Inter", sans-serif;
}

p {
  color: black;
  font-size: 11px;
  font-family: "Inter", sans-serif;
}


/* Content will be in these eventually */

.bar-left,
.bar-right {
  border-right: 1px solid black;
  box-sizing: border-box;
  height: 100vh;
  position: fixed;
  top: 0;
  left: 0;
  width: 48px;
  z-index: 10000;
}

.bar-right {
  border: none;
  border-left: 1px solid black;
  left: auto;
  right: 0;
}


/* Split Loop */

.split-loop {
  position: relative;
  margin: 0 48px;
}

.split-loop__left {
  width: 50%;
  overflow: auto;
  position: relative;
  max-height: 100vh;
}

.split-loop__right:before,
.split-loop__left:before {
  display: block;
  content: "";
  z-index: -1;
  height: 9999999px;
}

.split-loop__right {
  border-left: 1px solid black;
  box-sizing: border-box;
  position: fixed;
  right: 48px;
  bottom: 0;
  z-index: 5;
  width: calc(50% - 48px);
  overflow: auto;
  max-height: 100vh;
}

.project {
  box-sizing: border-box;
  border-bottom: 1px solid black;
  height: 600px;
  padding: 48px;
  position: absolute;
}

.project__media {
  /* height: 100px; */
}
<header class="bar-left"></header>

<div class="bar-right"></div>

<div class="view">
  <div class="grid split-loop" style="max-height: 100vh">
    <div class="split-loop__left">
      <div class="grid__item project">
        <img src="https://www.fillmurray.com/600/400" alt="" class="project__media" />
        <h2 class="project__title">Project Title</h2>
        <p class="project__desc">Short Description</p>
      </div>

      <div class="grid__item project">
        <img src="https://www.fillmurray.com/g/600/400" alt="" class="project__media" />
        <h2 class="project__title">Project Title</h2>
        <p class="project__desc">Short Description</p>
      </div>

      <div class="grid__item project">
        <img src="https://www.fillmurray.com/600/400" alt="" class="project__media" />
        <h2 class="project__title">Project Title</h2>
        <p class="project__desc">Short Description</p>
      </div>
    </div>

    <div class="split-loop__right">
      <div class="grid__item project">
        <img src="https://www.fillmurray.com/g/600/400" alt="" class="project__media" />
        <h2 class="project__title">Project Title</h2>
        <p class="project__desc">Short Description</p>
      </div>

      <div class="grid__item project">
        <img src="https://www.fillmurray.com/600/400" alt="" class="project__media" />
        <h2 class="project__title">Project Title</h2>
        <p class="project__desc">Short Description</p>
      </div>

      <div class="grid__item project">
        <img src="https://www.fillmurray.com/g/600/400" alt="" class="project__media" />
        <h2 class="project__title">Project Title</h2>
        <p class="project__desc">Short Description</p>
      </div>
    </div>
  </div>
</div>
Oleg Barabanov
  • 2,468
  • 2
  • 8
  • 17
  • Thanks! I'll have a play around with this later when I'm free. I noticed when I scroll over one column then switch to the other only one scrolls momentarily. The other thing I'm trying to get away from is not having a fixed height for each block if possible. Thanks again! :) – user1406440 Dec 19 '21 at 10:37
  • If the dimensions of the elements in the visible part change, it is necessary to recalculate the position of all elements and scroll. It is not necessary to do this at the time of scrolling, because it often has unpleasant effects. Hidden elements may well change their height, because when they are repositioned, their new height is calculated (using `.offsetHeight`). – Oleg Barabanov Dec 19 '21 at 15:00
  • The script is not the final solution - it is rather an example. The script has problems if all the elements fit into the visible area. It would also be nice to normalize the scroll after its completion, i.e. after stopping the scroll, reposition the elements in the middle to apply a small scroll. If necessary, there is no problem to implement it. – Oleg Barabanov Dec 19 '21 at 15:01
  • Thanks! I'm just getting back onto this. I know you said it's not final but this is great and is close to what I want to achieve. I just had a general question about the before/after heights - and what they do/are needed for? – user1406440 Apr 10 '22 at 13:12
  • @user1406440, Do I understand correctly that you mean the ":before" pseudo-elements with a large height? These elements are needed to have a large scroll (because you need extra space), in order for smooth smooth scrolling to work successfully (especially on mobile devices). – Oleg Barabanov Apr 13 '22 at 13:40
  • 1
    @user1406440, Since you can't scroll to a negative position (-500px for example) you need indents so that there is extra space at the top and bottom. To avoid jerking when scrolling, you have to make it so that moving elements does not affect the scrolling. That is why, as a simple variant, with a pseudo-element `:before` we create a huge scroll and initially place elements in the middle (with absolute positioning), so that there is a huge margin at the top and bottom for smooth scrolling and elements simply move by changing CSS values `top`. I hope I was able to explain the details :) – Oleg Barabanov Apr 13 '22 at 13:41
  • That's great I just wondered! And I guess with the large value it will never be exceeded :) – user1406440 Apr 13 '22 at 18:42