Mike Sav
Mike Sav

Reputation: 15291

Smooth scrolling when scrolling down a div using JavaScript, scrolling back up is janky and slow

I have a simple page where I am going to place a large form on the right side, and a navigation menu on the left allowing the user to jump to sections of the large form (this could also be used for large articles too - for now to mock the size I have added a large bottom padding with CSS). The right column has been styled to fit the height of the window using height: 100vh.

When clicking a link on the right and the section offsetTop is greater than its parent's scrollTop the scrolling is smooth. I use a JavaScript setTimeout and I set the timeout's interval using a loop... this is great! However should I want to scroll up, that being the section's offsetTop is less than its parent's scrollTop I get problems - it is janky, slow and doesn't really work... I have been looking at this for too long so if someone can help I'd be most appreciative. This is my JavaScript function

function scrollToAchor(where, event) {
  event.stopPropagation();
  var element = document.querySelector('.aims-form__form').querySelector('a[name="' + where + '"]');
  var to = element.offsetTop - 30;
  var from = document.querySelector('.aims-form__form').scrollTop
  let timeOut = 0;

  if(to >= from) {
    for(let i = from; i <= to; i+=5) {
      // this works and is great!!!
      setTimeout(function () {
        document.querySelector('.aims-form__form').scrollTop = i;
      }, i/2);
    }
  } else {

    for(let k = from; k >= to; k-=5) {
      // I need to set the timeout Interval so the animation is smooth but it is really slow
      // with the above setInterval above we are counting up... How do I get this smooth
      timeOut = timeOut + (to + 15) / 2;

      setTimeout(function () {
        document.querySelector('.aims-form__form').scrollTop = i;
      }, timeOut);
    }
  }
}

This shouldn't phase me but I can't get my head around it and now I have a mental block. To understand the scrolling function better here is the HTML, however to get a full picture (including the necessary CSS) here is a jsbin of the full thing https://jsbin.com/nidevaj/edit?html,css,js,output

  <div class="aims-form">
    <div class="aims-form__navigation">
      <ul>
        <li>
          <a onClick="scrollToAchor('section1', event)" class="aims-form__anchor">Section 1</a>
        </li>
        <li>
          <a onClick="scrollToAchor('section2', event)" class="aims-form__anchor">Section 2</a>
        </li>
        <li>
          <a onClick="scrollToAchor('section3', event)" class="aims-form__anchor">Section 3</a>
        </li>
        <li>
          <a onClick="scrollToAchor('section4', event)" class="aims-form__anchor">Section 4</a>
        </li>
      </ul>
    </div>
    <div class="aims-form__form">
      <section>
        <a name="section1"></a>
        Section 1 
        <br>
        ... content lots of content
      </section>
      <section>
        <a name="section2"></a>
        Section 2 
        <br>
        ... content lots of content
      </section>
      <section>
        <a name="section3"></a>
        Section 3 
        <br>
        ... content lots of content
      </section>
      <section>
      <a name="section4"></a>
        Section 4 
        <br>
        ... content lots of content
      </section>
  </div>
</div>

Upvotes: 1

Views: 838

Answers (1)

Unmitigated
Unmitigated

Reputation: 89224

You can set the CSS property scroll-behavior to smooth (supported by most modern browsers) which removes the need for Javascript. Just give the anchor tags a href of # plus the name of the anchor tag to scroll to and allow the normal scrolling to occur, except much more smooth.

section{
  margin: 500px 0px;
}
html, body{
  scroll-behavior: smooth;
}
 <div class="aims-form">
    <div class="aims-form__navigation">
      <ul>
        <li>
          <a href="#section1">Section 1</a>
        </li>
        <li>
          <a href="#section2">Section 2</a>
        </li>
        <li>
          <a href="#section3">Section 3</a>
        </li>
        <li>
          <a href="#section4">Section 4</a>
        </li>
      </ul>
    </div>
    <div class="aims-form__form">
      <section>
        <a name="section1"></a>
        Section 1 
        <br>
        ... content lots of content
      </section>
      <section>
        <a name="section2"></a>
        Section 2 
        <br>
        ... content lots of content
      </section>
      <section>
        <a name="section3"></a>
        Section 3 
        <br>
        ... content lots of content
      </section>
      <section>
      <a name="section4"></a>
        Section 4 
        <br>
        ... content lots of content
      </section>
  </div>
</div>

If you want to use Javascript, you can use a for loop with window.scrollTo and setTimeout as well as setInterval for smooth scrolling. I have written a function for this. To scroll to an element, provide the element's offsetTop as the pos argument.

function scrollToSmoothly(pos, time) {
  if (isNaN(pos)) {
    throw "Position must be a number";
  }
  if (pos < 0) {
    throw "Position can not be negative";
  }
  var currentPos = window.scrollY || window.screenTop;
  if (currentPos < pos) {
    var t = 10;
    for (let i = currentPos; i <= pos; i += 10) {
      t += 10;
      setTimeout(function() {
        window.scrollTo(0, i);
      }, t / 2);
    }
  } else {
    time = time || 2;
    var i = currentPos;
    var x;
    x = setInterval(function() {
      window.scrollTo(0, i);
      i -= 10;
      if (i <= pos) {
        clearInterval(x);
      }
    }, time);
  }
}

section{
  margin: 500px 0px;
}
<div class="aims-form">
    <div class="aims-form__navigation">
      <ul>
        <li>
          <a onClick="scrollToAchor('section1', event)" class="aims-form__anchor">Section 1</a>
        </li>
        <li>
          <a onClick="scrollToAchor('section2', event)" class="aims-form__anchor">Section 2</a>
        </li>
        <li>
          <a onClick="scrollToAchor('section3', event)" class="aims-form__anchor">Section 3</a>
        </li>
        <li>
          <a onClick="scrollToAchor('section4', event)" class="aims-form__anchor">Section 4</a>
        </li>
      </ul>
    </div>
    <div class="aims-form__form">
      <section>
        <a name="section1"></a>
        Section 1 
        <br>
        ... content lots of content
      </section>
      <section>
        <a name="section2"></a>
        Section 2 
        <br>
        ... content lots of content
      </section>
      <section>
        <a name="section3"></a>
        Section 3 
        <br>
        ... content lots of content
      </section>
      <section>
      <a name="section4"></a>
        Section 4 
        <br>
        ... content lots of content
        <button onClick="scrollToTop()">
        Back To Top
        </button>
      </section>
  </div>
</div>
<script>
function scrollToSmoothly(pos, time) {
  if (isNaN(pos)) {
    throw "Position must be a number";
  }
  if (pos < 0) {
    throw "Position can not be negative";
  }
  var currentPos = window.scrollY || window.screenTop;
  if (currentPos < pos) {
    var t = 10;
    for (let i = currentPos; i <= pos; i += 10) {
      t += 10;
      setTimeout(function() {
        window.scrollTo(0, i);
      }, t / 2);
    }
  } else {
    time = time || 2;
    var i = currentPos;
    var x;
    x = setInterval(function() {
      window.scrollTo(0, i);
      i -= 10;
      if (i <= pos) {
        clearInterval(x);
      }
    }, time);
  }
}
function scrollToAchor(where, event) {
  event.stopPropagation();
  var element = document.querySelector('.aims-form__form').querySelector('a[name="' + where + '"]');
  scrollToSmoothly(element.offsetTop); 
}
function scrollToTop(){
  scrollToSmoothly(0);
}
</script>

If you want to have the contents of a div scroll smoothly, you can use the above function, replacing window.scrollTo with elem.scrollTop and passing in the element's offsetTop as the pos to scroll to the element.

section{
  margin: 500px 0px;
}
<div style="border: 1px solid black; margin: auto; height: 250px; width: 50%; overflow-y: auto;" id="parent">
<div class="aims-form">
    <div class="aims-form__navigation">
      <ul>
        <li>
          <a onClick="scrollToAnchor('section1', event)" class="aims-form__anchor">Section 1</a>
        </li>
        <li>
          <a onClick="scrollToAnchor('section2', event)" class="aims-form__anchor">Section 2</a>
        </li>
        <li>
          <a onClick="scrollToAnchor('section3', event)" class="aims-form__anchor">Section 3</a>
        </li>
        <li>
          <a onClick="scrollToAnchor('section4', event)" class="aims-form__anchor">Section 4</a>
        </li>
      </ul>
    </div>
    <div class="aims-form__form">
      <section>
        <a name="section1"></a>
        Section 1 
        <br>
        ... content lots of content
      </section>
      <section>
        <a name="section2"></a>
        Section 2 
        <br>
        ... content lots of content
      </section>
      <section>
        <a name="section3"></a>
        Section 3 
        <br>
        ... content lots of content
      </section>
      <section>
      <a name="section4"></a>
        Section 4 
        <br>
        ... content lots of content
        <p/>
        <button onClick="scrollToTopOfDiv()">
        Back To Top
        </button>
      </section>
  </div>
</div>
</div>
<script>
function scrollToSmoothlyInsideElement(elem, pos, time) {
  if (isNaN(pos)) {
    throw "Position must be a number";
  }
  if (pos < 0) {
    throw "Position can not be negative";
  }
  var currentPos = elem.scrollTop;
  if (currentPos < pos) {
    var t = 10;
    for (let i = currentPos; i <= pos; i += 10) {
      t += 10;
      setTimeout(function() {
       elem.scrollTop = i;
      }, t / 2);
    }
  } else {
    time = time || 2;
    var i = currentPos;
    var x;
    x = setInterval(function() {
      elem.scrollTop = i;
      i -= 10;
      if (i <= pos) {
        clearInterval(x);
      }
    }, time);
  }
}
function scrollToAnchor(where, event){
    event.stopPropagation();
  var element = document.querySelector('.aims-form__form').querySelector('a[name="' + where + '"]');
  var parent = document.querySelector('#parent');
  scrollToSmoothlyInsideElement(parent, element.offsetTop);
}
function scrollToTopOfDiv(){
  var elem = document.querySelector('#parent');
  scrollToSmoothlyInsideElement(elem, 0);
}
</script>


For a more comprehensive list of methods for smooth scrolling, see my answer here.

Upvotes: 1

Related Questions