Search code examples
javascriptscrollformula

Custom scrollbars thumb goes out of bounds


I am trying to create a thumb scroller that scrolls together with the default scroller (with animation). I wan finally able to implement it, be the only problem is, the custom scroller doesn't scroll at the exact position. If you scroll all the way down, the thumb scroller goes past its parent element.

Also, the thumb scroller is larger than the default scroller. Here's the math:

scrollBarThumb.style.height = (innerWrapper.parentElement.offsetHeight * innerWrapper.parentElement.offsetHeight / innerWrapper.scrollHeight) + 'px';
scrollBarPosition = clamp(scrollBarPosition, 0, scrolledToBottom)
scrollBarPosition = scrollBar.offsetHeight * scrollBarPosition / innerWrapper.scrollHeight;

The formulas I used are the accepted formulas for custom scrollers.

The two problems are: (they may be one issue, because I think if the height gets fixed, then the scrolling position will automatically get fixed as well.) First, the scrollers thumb is larger than the default. Second, the scrollers thumb goes past its parent when you scroll all the way down.

What am I doing wrong, and how can I fix it?

JSFiddle

console.clear();

var innerWrapper = document.getElementById('innerWrapper');
var scrollBar = document.getElementById('scrollbar');
var scrollBarThumb = scrollBar.firstElementChild

scrollBarThumb.style.height = (innerWrapper.parentElement.offsetHeight * innerWrapper.parentElement.offsetHeight / innerWrapper.scrollHeight) + 'px';

innerWrapper.addEventListener('mousewheel', handleScroll);
innerWrapper.addEventListener('DOMMouseScroll', handleScroll);

var duration = 35,
  scrollSpeed = 2,
  animateAmount = 30;

var scrolledToBottom = 0,
  scrollDirection = 0, // 1 = scroll down, -1 = scroll up
  animationID;




function handleScroll(e) {
  // Cancel previous animation
  cancelAnimationFrame(animationID);
  // Scroll faster
  scrollSpeed += 2;

  // Reason for negative `-.wheelDelta` because Firefox
  // return oposite value. See http://phrogz.net/js/wheeldelta.html
  // Get 1 or -1
  var delta = Math.max(-1, Math.min(1, (-e.wheelDelta || e.detail)));
  // Check if scroll direction changed
  if (scrollDirection != delta) {
    scrollSpeed = 2; // Start slowly - restart speed
    scrollDirection = delta;
  }
  var start = innerWrapper.parentElement.scrollTop,
    end = start + animateAmount * scrollSpeed * delta, // Where to end the scroll
    change = end - start, // base change in one scroll
    step = 0, // current step in animation
    tempScrollPosition; // Cannot assign any number yet (i.e. 0), because `scrollPosition` may be 0.
  // Get amount of scrolled to bottom
  scrolledToBottom = innerWrapper.scrollHeight - innerWrapper.parentElement.offsetHeight;

  animationID = requestAnimationFrame(smoothScrollAnim); // Start animation

  function smoothScrollAnim() {
    animationID = requestAnimationFrame(smoothScrollAnim); // Restart animation
    // Get scroll position
    var scrollBarPosition = easeOut(step++, start, change, duration);
    scrollBarPosition = clamp(scrollBarPosition, 0, scrolledToBottom)

    scrollBarPosition = scrollBar.offsetHeight * scrollBarPosition / innerWrapper.scrollHeight;

    // Apply scroll movement
    scrollBarThumb.style.top = scrollBarPosition + 'px';

    // Check if scroll finished (either animation finished, or bumped to top or bottom)
    if (step >= duration || tempScrollPosition === scrollBarPosition) {
      // Clean up
      tempScrollPosition = null;
      scrollSpeed = 2;
      cancelAnimationFrame(animationID);
    } else {
      tempScrollPosition = scrollBarPosition;
    }
  }
}


function easeOut(time, begin, change, duration) {
  time /= duration;
  return -change * time * (time - 2) + begin;
}

function clamp(val, min, max) {
  if (typeof min !== 'number') min = 0;
  if (typeof max !== 'number') max = 1;
  return Math.min(Math.max(val, min), max);
}
html {
  height: 100%;
  overflow-y: hidden;
}
body {
  height: 100%;
  overflow-y: hidden;
  display: flex;
}
#outerWrapper {
  height: 400px;
  overflow: auto;
  background-color: black;
}
#content {
  background-image: url("http://images.freeimages.com/images/premium/previews/3037/30376024-beautiful-flower-portrait.jpg");
  width: 400px;
}
#scrollbar {
  height: 400px;
  width: 50px;
  background-color: orange;
  border: 2px solid green;
}
#scrollbar_thumb {
  background-color: yellow;
  border: 2px solid blue;
  position: relative;
}
<div id="outerWrapper">
  <div id="innerWrapper">
    <div id="content">
      Lorem ipsum dolor sit amet consectetuer laoreet faucibus id ut et. Consequat Ut tellus enim ante nulla molestie vitae sem interdum turpis. Fames ridiculus cursus pellentesque Vestibulum justo sem lorem neque accumsan nulla. Lacinia Suspendisse vitae libero
      sem et laoreet risus Sed condimentum Cras. Nunc massa mauris tempor dolor pulvinar justo neque dui ipsum vitae. Lacinia dui cursus pellentesque Vestibulum justo sem lorem neque accumsan nulla. Lacinia Suspendisse vitae libero sem et laoreet risus
      Sed condimentum Cras. Nunc massa mauris tempor dolor pulvinar justo neque dui ipsum vitae. Lacinia dui scelerisque Sed convallis nonummy orci Vestibulum orci tempusLorem ipsum dolor sit amet consectetuer laoreet faucibus id ut et. Consequat Ut tellus
      enim ante nulla molestie vitae sem interdum turpis. Fames ridiculus cursus pellentesque Vestibulum justo sem lorem neque accumsan nulla. Lacinia Suspendisse vitae libero sem et laoreet risus Sed condimentum Cras. Nunc massa mauris tempor dolor pulvinar
      justo neque dui ipsum vitae. Lacinia dui scelerisque Sed convallis nonummy orci Vestibulum orci tempusLorem ipsum dolor sit amet consectetuer laoreet faucibus id ut et. Consequat Ut tellus enim ante nulla molestie vitae sem interdum turpis. Fames
      ridiculus cursus pellentesque Vestibulum justo sem lorem neque accumsan nulla. Lacinia Suspendisse vitae libero sem et laoreet risus Sed condimentum Cras. Nunc massa mauris tempor dolor pulvinar justo neque dui ipsum vitae. Lacinia dui scelerisque
      Sed convallis nonummy orci Vestibulum orci tempusLorem ipsum dolor sit amet consectetuer laoreet faucibus id ut et. Consequat Ut tellus enim ante nulla molestie vitae sem interdum turpis. Fames ridiculus cursus pellentesque Vestibulum justo sem
      lorem neque accumsan nulla. Lacinia Suspendisse vitae libero sem et laoreet risus Sed condimentum Cras. Nunc massa mauris tempor dolor Lorem ipsum dolor sit amet consectetuer laoreet faucibus id ut et. Consequat Ut tellus enim ante nulla molestie
      vitae sem interdum turpis. Fames ridiculus cursus pellentesque Vestibulum justo sem lorem neque accumsan nulla. Lacinia Suspendisse vitae libero sem et laoreet risus Sed condimentum Cras. Nunc massa mauris tempor dolor pulvinar justo neque dui ipsum
      vitae. Lacinia dui scelerisque Sed convallis nonummy orci Vestibulum orci tempusLorem ipsum dolor sit amet consectetuer laoreet faucibus id ut et. Consequat Ut tellus enim ante nulla molestie vitae sem interdum turpis. Fames ridiculus cursus pellentesque
      Vestibulum justo sem lorem neque accumsan nulla. Lacinia Suspendisse vitae libero sem et laoreet risus Sed condimentum Cras. Nunc massa mauris tempor dolor pulvinar justo neque dui ipsum vitae. Lacinia dui scelerisque Sed convallis nonummy orci
      Vestibulum orci tempusLorem ipsum dolor sit amet consectetuer laoreet faucibus id ut et. Consequat Ut tellus enim ante nulla molestie vitae sem interdum turpis. Fames ridiculus cursus pellentesque Vestibulum justo sem lorem neque accumsan nulla.
      Lacinia Suspendisse vitae libero sem et laoreet risus Sed condimentum Cras. Nunc massa mauris tempor dolor pulvinar justo neque dui ipsum vitae. Lacinia dui scelerisque Sed convallis nonummy orci Vestibulum orci tempusLorem ipsum dolor sit amet
      consectetuer laoreet faucibus id ut et. Consequat Ut tellus enim ante nulla molestie vitae sem interdum turpis. Fames ridiculus cursus pellentesque Vestibulum justo sem lorem neque accumsan nulla. Lacinia Suspendisse vitae libero sem et laoreet
      risus Sed condimentum Cras. Nunc massa mauris tempor dolor pulvinar justo neque dui ipsum vitae. Lacinia dui scelerisque Sed convallis nonummy orci Vestibulum orci tempusLorem ipsum dolor sit amet consectetuer laoreet faucibus id ut et. Consequat
      Ut tellus enim ante nulla molestie vitae sem interdum turpis. Fames ridiculus cursus pellentesque Vestibulum justo sem lorem neque accumsan nulla. Lacinia Suspendisse vitae libero sem et laoreet risus Sed condimentum Cras. Nunc massa mauris tempor
      dolor
    </div>
  </div>

</div>
<div id="scrollbar">
  <div id="scrollbar_thumb"></div>
</div>


Solution

  • For the scroll simply register the "scroll" event,
    For the catching-delay effect use CSS3 transition
    For the simple math involved see the example below:

    const el = (sel, par) => (par || document).querySelector(sel);
    
    const
      elContent = el("#content"),
      elHandler = el("#handler");
    
    // FROM ELEMENT SCROLL TO HANDLER POSITION
    const moveScrollbar = () => {
      const
        height = elContent.clientHeight,
        scrollHeight = elContent.scrollHeight,
        handlerHeight = height ** 2 / scrollHeight,
        handlerTop = elContent.scrollTop / height * handlerHeight;
    
      Object.assign(elHandler.style, {
        height: `${handlerHeight}px`,
        top: `${handlerTop}px`
      });
    }
    
    moveScrollbar(); // At init
    elContent.addEventListener("scroll", moveScrollbar); // and on scroll
    * { margin: 0; box-sizing: border-box; }
    
    #area {
      display: flex;
    }
    
    #content {
      height: 160px;
      width: 300px;
      font-size: 3rem;
      background: #eee;
      padding: 1rem;
    }
    
    #content {
      overflow-y: scroll;
      scrollbar-width: none;
      -ms-overflow-style: none;
    }
    
    #content::-webkit-scrollbar {
      width: 0;
      height: 0;
    }
    
    #scrollbar {
      flex: none;
      position: relative;
      background: #333;
      width: 1rem;
    }
    
    #handler {
      position: absolute;
      top: 0;
      background: orange;
      width: 100%;
      transition: 0.2s; /* smooth move */
    }
    <div id="area">
      <div id="content">
        Scroll and see the custom scrollbar move.<br>Lorem ipsum dolor sit amet consectetuer laoreet faucibus id ut et. Consequat Ut tellus enim ante nulla molestie vitae sem interdum turpis.
      </div>
    
      <div id="scrollbar">
        <div id="handler"></div>
      </div>
    </div>

    Then, to make the handler draggable, here's the formula:

    // FROM HANDLER POSITION TO ELEMENT SCROLL:
    const
        height = elContent.clientHeight,
        scrollHeight = elContent.scrollHeight,
        scrollPos = scrollHeight / height * elHandler.clientTop;