UI Components Medium
Range Slider
Custom-styled range sliders — single value with floating tooltip, dual-handle range, stepped with tick marks, and color variants.
Open in Lab
MCP
css vanilla-js
Targets: JS HTML
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--bg: #050910;
--card: #0d1117;
--border: rgba(255, 255, 255, 0.08);
--text: #f2f6ff;
--muted: #475569;
--accent: #38bdf8;
--track-bg: rgba(255, 255, 255, 0.08);
--thumb-size: 18px;
--track-h: 5px;
}
body {
background: var(--bg);
color: var(--text);
font-family: system-ui, -apple-system, sans-serif;
min-height: 100vh;
padding: 3rem 1.5rem;
}
.demo {
max-width: 480px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 2.5rem;
}
.demo-title {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 0.25rem;
}
.demo-sub {
font-size: 0.875rem;
color: var(--muted);
margin-bottom: 1rem;
}
.section {
display: flex;
flex-direction: column;
gap: 1rem;
}
.section-label {
font-size: 0.72rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.09em;
color: var(--muted);
}
/* ── Shared input ── */
.rs-wrap {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.rs-input {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: var(--track-h);
border-radius: 999px;
outline: none;
cursor: pointer;
background: linear-gradient(
to right,
var(--accent) 0%,
var(--accent) var(--fill, 50%),
var(--track-bg) var(--fill, 50%),
var(--track-bg) 100%
);
}
.rs-input::-webkit-slider-thumb {
-webkit-appearance: none;
width: var(--thumb-size);
height: var(--thumb-size);
border-radius: 50%;
background: var(--accent);
border: 3px solid #050910;
box-shadow: 0 0 0 1px var(--accent), 0 2px 8px rgba(56, 189, 248, 0.35);
transition: transform 0.12s, box-shadow 0.12s;
cursor: grab;
}
.rs-input::-webkit-slider-thumb:active {
transform: scale(1.15);
cursor: grabbing;
}
.rs-input::-moz-range-thumb {
width: var(--thumb-size);
height: var(--thumb-size);
border-radius: 50%;
background: var(--accent);
border: 3px solid #050910;
box-shadow: 0 0 0 1px var(--accent);
cursor: grab;
}
/* ── Tooltip ── */
.rs-tooltip-wrap {
position: relative;
padding-top: 2rem;
}
.rs-tooltip {
position: absolute;
top: 0;
left: var(--fill, 50%);
transform: translateX(-50%);
background: var(--accent);
color: #050910;
font-size: 0.75rem;
font-weight: 700;
padding: 0.2rem 0.5rem;
border-radius: 6px;
pointer-events: none;
white-space: nowrap;
transition: left 0.06s;
}
.rs-tooltip::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 4px solid transparent;
border-top-color: var(--accent);
}
/* ── Labels row ── */
.rs-labels {
display: flex;
justify-content: space-between;
font-size: 0.75rem;
color: var(--muted);
}
/* ── Stepped ticks ── */
.rs-ticks {
display: flex;
justify-content: space-between;
font-size: 0.68rem;
color: var(--muted);
}
/* ── Dual handle ── */
.rs-dual-wrap {
position: relative;
height: var(--thumb-size);
display: flex;
align-items: center;
}
.rs-dual-track {
position: absolute;
left: 0;
right: 0;
height: var(--track-h);
background: var(--track-bg);
border-radius: 999px;
}
.rs-dual-fill {
position: absolute;
height: 100%;
background: var(--accent);
border-radius: 999px;
}
.rs-dual-input {
position: absolute;
left: 0;
right: 0;
width: 100%;
pointer-events: none;
background: transparent;
}
.rs-dual-input::-webkit-slider-thumb {
pointer-events: all;
}
.rs-dual-input::-moz-range-thumb {
pointer-events: all;
}
/* ── Color variants ── */
.rs-colors {
gap: 1.25rem;
}
.rs-blue {
--accent: #38bdf8;
}
.rs-green {
--accent: #4ade80;
}
.rs-red {
--accent: #f87171;
}(function () {
"use strict";
function pct(val, min, max) {
return ((val - min) / (max - min)) * 100;
}
// ── Single with tooltip ──────────────────────────────────────────────────────
(function () {
const input = document.getElementById("rs-single");
const tooltip = document.getElementById("tt-single");
if (!input || !tooltip) return;
function update() {
const p = pct(input.value, input.min, input.max);
input.style.setProperty("--fill", p + "%");
tooltip.style.setProperty("--fill", p + "%");
tooltip.textContent = input.value;
}
input.addEventListener("input", update);
update();
})();
// ── Stepped ─────────────────────────────────────────────────────────────────
(function () {
const input = document.getElementById("rs-stepped");
const tooltip = document.getElementById("tt-stepped");
if (!input || !tooltip) return;
function update() {
const p = pct(input.value, input.min, input.max);
input.style.setProperty("--fill", p + "%");
tooltip.style.setProperty("--fill", p + "%");
tooltip.textContent = input.value;
}
input.addEventListener("input", update);
update();
})();
// ── Dual handle ─────────────────────────────────────────────────────────────
(function () {
const minInput = document.getElementById("rs-min");
const maxInput = document.getElementById("rs-max");
const fill = document.getElementById("rs-dual-fill");
const lblMin = document.getElementById("lbl-min");
const lblMax = document.getElementById("lbl-max");
if (!minInput || !maxInput || !fill) return;
function update() {
let lo = parseFloat(minInput.value);
let hi = parseFloat(maxInput.value);
if (lo > hi) {
[lo, hi] = [hi, lo];
minInput.value = lo;
maxInput.value = hi;
}
const min = parseFloat(minInput.min);
const max = parseFloat(minInput.max);
const left = pct(lo, min, max);
const right = pct(hi, min, max);
fill.style.left = left + "%";
fill.style.width = right - left + "%";
// Dual inputs use transparent bg — reset CSS fill var so no gradient shows
minInput.style.setProperty("--fill", "0%");
maxInput.style.setProperty("--fill", "0%");
if (lblMin) lblMin.textContent = "$" + lo;
if (lblMax) lblMax.textContent = "$" + hi;
}
minInput.addEventListener("input", update);
maxInput.addEventListener("input", update);
update();
})();
// ── Color variants ───────────────────────────────────────────────────────────
document.querySelectorAll(".rs-blue, .rs-green, .rs-red").forEach(function (input) {
function update() {
input.style.setProperty("--fill", pct(input.value, input.min, input.max) + "%");
}
input.addEventListener("input", update);
update();
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Range Slider</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="demo">
<h1 class="demo-title">Range Slider</h1>
<p class="demo-sub">Single, dual-handle, stepped, and color variants.</p>
<!-- Single with floating tooltip -->
<section class="section">
<p class="section-label">Single — floating tooltip</p>
<div class="rs-wrap">
<div class="rs-tooltip-wrap">
<span class="rs-tooltip" id="tt-single">50</span>
<input class="rs-input" type="range" id="rs-single" min="0" max="100" value="50" />
</div>
<div class="rs-labels">
<span>0</span><span>100</span>
</div>
</div>
</section>
<!-- Dual handle -->
<section class="section">
<p class="section-label">Dual handle — price range</p>
<div class="rs-wrap">
<div class="rs-dual-wrap" id="rs-dual">
<div class="rs-dual-track">
<div class="rs-dual-fill" id="rs-dual-fill"></div>
</div>
<input class="rs-input rs-dual-input" type="range" id="rs-min" min="0" max="1000" value="200" />
<input class="rs-input rs-dual-input" type="range" id="rs-max" min="0" max="1000" value="750" />
</div>
<div class="rs-labels">
<span id="lbl-min">$200</span>
<span id="lbl-max">$750</span>
</div>
</div>
</section>
<!-- Stepped with ticks -->
<section class="section">
<p class="section-label">Stepped — with tick marks</p>
<div class="rs-wrap">
<div class="rs-tooltip-wrap">
<span class="rs-tooltip" id="tt-stepped">5</span>
<input class="rs-input" type="range" id="rs-stepped" min="0" max="10" value="5" step="1" list="step-ticks" />
<datalist id="step-ticks">
<option value="0"></option><option value="1"></option><option value="2"></option>
<option value="3"></option><option value="4"></option><option value="5"></option>
<option value="6"></option><option value="7"></option><option value="8"></option>
<option value="9"></option><option value="10"></option>
</datalist>
</div>
<div class="rs-ticks">
<span>0</span><span>1</span><span>2</span><span>3</span><span>4</span>
<span>5</span><span>6</span><span>7</span><span>8</span><span>9</span><span>10</span>
</div>
</div>
</section>
<!-- Color variants -->
<section class="section">
<p class="section-label">Color variants</p>
<div class="rs-wrap rs-colors">
<input class="rs-input rs-blue" type="range" min="0" max="100" value="60" />
<input class="rs-input rs-green" type="range" min="0" max="100" value="40" />
<input class="rs-input rs-red" type="range" min="0" max="100" value="75" />
</div>
</section>
</div>
<script src="script.js"></script>
</body>
</html>Range Slider
Styled range inputs with live value display and a dual-handle variant for min/max selection.
Variants
| Variant | Description |
|---|---|
| Single + tooltip | Floating label above thumb shows current value |
| Dual handle | Two thumbs for selecting a range (e.g. price filter) |
| Stepped | Tick marks at each step value |
| Color variants | Blue / green / red tracks |
Implementation
Uses native <input type="range"> for accessibility — styling applied via ::-webkit-slider-thumb and ::-webkit-slider-runnable-track. Track fill uses a CSS linear-gradient updated on input events.