[JavaScript] レンジスライダーに現在値表示を追従させたい

inputタグには様々な入力方式に対応したtype属性が用意されています。
その中に、一定範囲の値をスライダーで設定するレンジスライダーという方式があります。
このレンジスライダー、デフォルトのままでは設定した値の内容がわかりにくいなど、少し使い勝手が悪いです。
CSS、JavaScriptを用いて、現在値の表示エリアを「つまみ」部分に追従するスライダーにすることで、見た目や使い勝手を改善することができます。
サンプルコード
html
<div class="range_slider">
    <span class="range_slider_min"></span>
    <div class="range_slider_input">
        <div class="range_slider_input_current">
            <span></span>
        </div>
        <input type="range" name="xxx" min="100" max="300" value="200" step="1">
    </div>
    <span class="range_slider_max"></span>
</div>
CSS
.range_slider {
    display: flex;
    padding: 30px 0px 0px;
}
.range_slider_min,
.range_slider_max {
    padding: 0px 10px;
    font-size: 1.2rem;
}
.range_slider_input {
    position: relative;
    width: 100%;
}
.range_slider_input_current {
    position: relative;
    margin: 0px auto;
    width: 97.5%;
}
.range_slider_input_current span {
    position: absolute;
    top: -30px;
    left: 50%;
    transform: translateX(-50%);
    padding: 0px 1em;
    border-radius: 5px;
    background-color: #3388dd;
    color: #fff;
    font-size: 1.2rem;
    font-weight: bold;
}
.range_slider_input input[type=range] {
    position: absolute;
    top: 50%;
    left: 0px;
    transform: translateY(-50%);
    width: 100%;
    border: none;
    -webkit-appearance: none;
}
.range_slider_input input[type=range]:focus {
    outline: 0px;
}
.range_slider_input input[type=range]::-webkit-slider-runnable-track {
    background: #888;
    height: 2px;
}
.range_slider_input input[type=range]::-moz-range-track {
    background: #888;
    height: 2px;
}
.range_slider_input input[type=range]::-webkit-slider-thumb {
    -webkit-appearance: none;
    color: #fff;
    height: 20px;
    width: 20px;
    background: #3388dd;
    margin-top: -10px;
    border-radius: 50%;
    cursor: pointer;
}
.range_slider_input input[type=range]::-moz-range-thumb {
    color: #fff;
    height: 20px;
    width: 20px;
    background: #3388dd;
    margin-top: -10px;
    border-radius: 50%;
    cursor: pointer;
}
JavaScript
document.addEventListener('DOMContentLoaded', function() {
    const rangeItems = document.querySelectorAll('.range_slider');
    for(let i = 0; i < rangeItems.length; i++) {
        const rangeItem = rangeItems[i];
        const rangeItemInput = rangeItem.querySelector('input[type="range"]');
        const min = parseInt(rangeItemInput.getAttribute('min'));
        rangeItem.querySelector('.range_slider_min').innerText = min;
        const max = parseInt(rangeItemInput.getAttribute('max'));
        rangeItem.querySelector('.range_slider_max').innerText = max;
        const rangeItemCurrent = rangeItem.querySelector('.range_slider_input_current span');
        matchCurrent();
        rangeItemInput.addEventListener('input', function() {
            matchCurrent();
        }, false);
        function matchCurrent() {
            const current = parseInt(rangeItemInput.value);
            const ratio = ((current - min) / (max - min)) * 100;
            rangeItemCurrent.innerText = current;
            rangeItemCurrent.style.left = ratio + '%';
        }
    }
}, false);
サンプルコードの解説
html、CSS、JavaScriptの各コードについて、ポイントとなる要素を抜粋して解説します。
html
<div class="range_slider">
    <span class="range_slider_min"></span>
    <div class="range_slider_input">
        <div class="range_slider_input_current">
            <span></span>
        </div>
        <input type="range" name="xxx" min="100" max="300" value="200" step="1">
    </div>
    <span class="range_slider_max"></span>
</div>
range_sliderクラスで囲んだ中に、inputタグや、最小値を表示するクラス(range_slider_min)、最大値を表示するクラス(range_slider_max)、現在値を表示するクラス(range_slider_input_current)を用意しています。
CSS
.range_slider_input input[type=range]::-webkit-slider-runnable-track {
    background: #888;
    height: 2px;
}
.range_slider_input input[type=range]::-moz-range-track {
    background: #888;
    height: 2px;
}
.range_slider_input input[type=range]::-webkit-slider-thumb {
    -webkit-appearance: none;
    color: #fff;
    height: 20px;
    width: 20px;
    background: #3388dd;
    margin-top: -10px;
    border-radius: 50%;
    cursor: pointer;
}
.range_slider_input input[type=range]::-moz-range-thumb {
    color: #fff;
    height: 20px;
    width: 20px;
    background: #3388dd;
    margin-top: -10px;
    border-radius: 50%;
    cursor: pointer;
}
ブラウザごとにスライダーの表示形式が異なるため、その差分をならすためのスタイルを指定しています。
.range_slider_input_current {
    position: relative;
    margin: 0px auto;
    width: 97.5%;
}
現在値を表示するエリアのスタイルです。
widthを97.5%にして、エリアの可動する範囲をスライダーの長さよりも少し狭めています。
この指定がないと、可動範囲をスライダーの範囲と同じにすると、下図のようにつまみを左右に振ったときに少しずつ表示エリアがずれてしまいます。

対策として、表示エリアの可動範囲を少し狭めることでズレを抑制しています。
指定するwidthの値は、スライダーを設置する場所(スライダーの横幅)に応じて微調整が必要になります。適宜値を変えて使用してください。
JavaScript
function matchCurrent() {
    const current = parseInt(rangeItemInput.value);
    const ratio = ((current - min) / (max - min)) * 100;
    rangeItemCurrent.innerText = current;
    rangeItemCurrent.style.left = ratio + '%';
}
スライダーを動かしたときに呼ばれる関数です。
const変数「ratio」に、「現在値から最小値を引いた値」を「最大値から最小値を引いた」値で割り、スライダーの入力範囲内に対して現在値が何%なのか算出した値を格納します。
この値を使って現在値表示の「left」プロパティの値を変えることで、表示エリアの座標を移動しています。
ご質問など受け付けています
記事の中でわかりにくかったところ、もっと知りたかったこと、間違っていることなど、何でもお気軽にご連絡ください。
ご連絡は下記フォームを利用いただくか、ツイッターアカウント@flat8migi宛てでもOKです。