JSFiddle - React, Tailwind, and code Playground

HTML

<div id="frame" tabIndex="0">
    <div id="content">
        <div id="scrollable">
           ...

CSS

body {
    overflow: hidden;
}

#frame {
    width: 80%;
    height: 300px;
    margin: 0px auto 0px auto;
    display: flex;
    flex-direction: row;
    justify-content: flex-start;
    align-items: stretch;
    flex-wrap: nowrap;
}

#content {
    flex-grow: 1;
    flex-basis: 1px;
    position: relative;
    background-color: #e0e0e0;
    overflow: hidden;
}

#scrollable {
    position: absolute;
    top: 0px;
    left: 0px;
}

#scrollbar {
    flex-grow: 0;
    flex-basis: 20px;
    
    position: relative;
    width: 20px;
    
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    flex-wrap: nowrap;
}

#rail {
    flex-grow: 1;
    flex-basis: 1px;
    width: 10px;
    background-color: #f0f0f0;
    border-radius: 4px;
}

#dragger {
    position: absolute;
    top: 0px;
    left: 0px;
    width: 100%;
}

#bar {
    width: 10px;
    height: 30px;
    margin: 0px auto 0px auto;
    background-color: red;
    border-radius: 4px;
}

#hook {
    position: absolute;
    top: 0px;
    left: 0px;
    width: 0px;
    height: 0px;
}

.drag-on #hook {
    position: absolute;
    top: -1500px;
    left: -1500px;
    width: 3000px;
    height: 3000px;
    z-index: 255;
}

JavaScript

var model = {
    dragOn: false,
    scrollMaxV: 7.0, // スクロールの最大速度
    scrollMinV: 0.25, // スクロールの最小速度
    scrollVRatio: 0.03, // スクロール速度係数
    scrollUnit: 10, // ホイールイベントやキーダウンイベントでスクロールバーを動かす量
    scrollY: 0, // scrollableの現在のY座標
    posY: 0, // スクロールバーの現在のY座標
    cursorY: 0 // スクロールバーのドラッグ中に、最後に記録したマウスカーソルのY座標
};

function updateUI() {
    dragMode();
    posDragger();
    updateContentPos();
}

function dragMode() {
    if (model.dragOn) {
        $('#dragger').addClass('drag-on');
    } else {
        $('#dragger').removeClass('drag-on');
    }
}

function posDragger() {
    var elem = $('#dragger');
    elem.css("top", model.posY + "px");
}

function updateContentPos() {
    // model.posYと同じスクロール比率になるまでスクロールする。
    // スクロール速度は、posY - scrollYの10%にする。

    // 現時点でのスクロール目標座標を算出
    var scrollRatio = model.posY / ($('#scrollbar').height() - $('#bar').height());
    var destinationPos = -1 * ($('#scrollable').height() - $('#content').height()) * scrollRatio;
    
    var distance = destinationPos - model.scrollY;
    
    if (Math.abs(distance) > 2) { // 2px以内はスクロールを完了させる
        // スクロール速度は移動先座標までの距離に比例
        var v = distance * model.scrollVRatio;
        // 最大・最小速度に制限を掛ける
        if (Math.abs(v) < model.scrollMinV) 
            v = v > 0 ? model.scrollMinV : -1 * model.scrollMinV;
        else if (model.scrollMaxV < Math.abs(v))
            v = v > 0 ? model.scrollMaxV : -1 * model.scrollMaxV;
        console.log(v);
        model.scrollY += v;
        $('#scrollable').css("top", model.scrollY + "px");
        requestAnimationFrame(updateContentPos);
    } else {
        model.scrollY = destinationPos;
        $('#scrollable').css("top", model.scrollY + "px");
    }
}

function limitPosY() {
    if(model.posY < 0) {
        model.posY = 0;
        return;
    }
    
    var maxY = $('#scrollbar').height() - $('#bar').height();
    if (maxY < model.posY) {
        model.posY = maxY;
        return;
    }
}

// vが正の値なら下へ、0又は負の値なら上へスクロール
function scroll(v) {
 ...