Creating a Custom Range Slider with HTML, CSS, and JavaScript

Range sliders are a handy UI component for selecting a value within a specified range. While the default styling of range sliders provided by browsers might suffice for basic usage, customizing them can add a unique touch to your web application.

1. HTML Structure

<div class="slider">
    <input 
        type="range"
        min="0"
        max="100"
        step="1"
        name="custom_slider"
        id="custom_slider"
        value="50"
    >
    <span class="slider__value">
        <span class="slider__value__num">50</span>
        <span class="slider__value__unit">px</span>
    </span>
    <div class="slider__progress"></div>
</div>

2. CSS Styling

.slider {
    --value: 50;
    --min: 0;
    --max: 100;
    --primary-color: #2196f3;
    --value-a: Clamp( var(--min), var(--value, 0), var(--max) );
    --value-b: var(--value, 0);
    --completed-a: calc( (var(--value-a) - var(--min)) / (var(--max) - var(--min)) * 100 );
    --completed-b: calc( (var(--value-b) - var(--min)) / (var(--max) - var(--min)) * 100 );
    --cb: Max(var(--completed-a), var(--completed-b));
    --track-height: 4px;
    --thumb-size: 18px;
    --ticks-gap: 5px;
    --value-offset-y: var(--ticks-gap);
    display: inline-block;
    height: 22px;
    position: relative;
    z-index: 1;
    width: 300px;
}

.slider input[type=range] {
    appearance: none;
    width: 100%;
    height: 22px;
    margin: 0;
    position: absolute;
    left: 0;
    top: 0;
    cursor: grab;
    outline: none;
    background: none;
}

.slider input[type=range]::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: var(--thumb-size);
    height: var(--thumb-size);
    background: var(--primary-color);
    border-radius: 50%;
    border: 2px solid #FFF;
    box-shadow: 0 0 10px 1px rgba(0,0,0,0.2);
    cursor: pointer;
}

.slider input[type=range]::-moz-range-thumb {
    width: var(--thumb-size);
    height: var(--thumb-size);
    background: var(--primary-color);
    border-radius: 50%;
    border: 2px solid #FFF;
    box-shadow: 0 0 10px 1px rgba(0,0,0,0.2);
    cursor: pointer;
}

.slider input[type=range] + .slider__value {
    --value: var(--value-a);
    --x-offset: calc(var(--completed-a) * -1%);
    --pos: calc( ((var(--value) - var(--min)) / (var(--max) - var(--min))) * 100% );
    --flip: -1;
    position: absolute;
    left: var(--pos);
    z-index: 5;
    display: flex;
    font-size: 10px;
    font-weight: bold;
    transform: translate(var(--x-offset), calc( 150% * var(--flip) - (var(--y-offset, 0px) + var(--value-offset-y)) * var(--flip) ));
    pointer-events: none;
}

.slider input[type=range] ~ .slider__progress {
    --start-end: calc(var(--thumb-size) / 2);
    --clip-end: calc(100% - (var(--cb)) * 1%);
    --clip-start: 0;
    --clip: inset(-20px var(--clip-end) -20px var(--clip-start));
    position: absolute;
    left: var(--start-end);
    right: var(--start-end);
    top: calc( var(--ticks-gap) * var(--flip-y, 0) + var(--thumb-size) / 2 - var(--track-height) / 2 );
    height: calc(var(--track-height));
    background: transparent;
    pointer-events: none;
    z-index: -1;
    border-radius: 20px;
    border: 1px solid var(--primary-color);
}

.slider input[type=range] ~ .slider__progress::before {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    clip-path: var(--clip);
    top: 0;
    bottom: 0;
    background: var(--primary-color);
    z-index: 1;
    border-radius: inherit;
}

.slider input[type=range] ~ .slider__progress::after {
    content: "";
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    pointer-events: none;
    border-radius: inherit;
}

3. JavaScript Functionality

document.addEventListener( 'DOMContentLoaded', () => {

    const input = document.querySelector('.slider input[type=range]');

    input.addEventListener( 'input', () => {

        const value = input.value;

        input.parentNode.style.setProperty( '--value', value );

        const sliderValue = input.nextElementSibling;
        const sliderValueNum = sliderValue.querySelector('.slider__value__num');

        if ( sliderValueNum ) {
            sliderValueNum.textContent = value;
        }
    });
});