<div id="container">
  <div id="box"></div>
  <div id="content"></div>
</div>
<div id="display">0</div>
#container {
  width: 300px;
  height: 100px;
  overflow-y: scroll;
  position: relative;
  border: 1px solid black; 
}
#content {
  height: 800px;
}
#box {
  width: 20px;
  height: 20px;
  background: red;
  position: absolute;
}
const container = document.getElementById('container');
const box = document.getElementById('box');
const display = document.getElementById('display');

const speed = 3;
const minDuration = 1;
const maxDuration = 1000;
let current = 0;
let animationId = null;

function easeOutQuint(x) {
  return 1 - Math.pow(1 - x, 5);
}

container.addEventListener('scroll', () => {
  if (animationId) cancelAnimationFrame(animationId);
  
  const target = container.scrollTop;
  const start = current;
  const startTime = performance.now();
  const distance = Math.abs(target - start);
  const duration = Math.min(Math.max(distance * speed, minDuration), maxDuration);

  function animate() {
    const elapsed = performance.now() - startTime;
    const t = Math.min(elapsed / duration, 1);
    const easedT = easeOutQuint(t);
    current = start + (target - start) * easedT;
    
    box.style.top = current + 'px';
    display.textContent = Math.round(current);
    
    if (t < 1) {
      animationId = requestAnimationFrame(animate);
    }
  }

  animate();
});

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.