01 / 17
All 17 in 17 Tailwind CSS Range Sliders
- 01Tailwind CSS Dual-Handle Price Range Slider with Live Product Count
- 02Tailwind CSS Neumorphic Volume Slider with Soft UI Shadows
- 03Tailwind CSS Neon Glow Range Slider for Dark Mode UI
- 04Tailwind CSS Emoji Mood Slider for Feedback & CSAT Surveys
- 05Tailwind CSS Multi-Step Pricing Slider with Snapping Milestones
- 06Tailwind CSS Video Player Seekbar with Hover Scrub & Chapters
- 07Tailwind CSS Vertical Range Sliders — 10-Band Audio Equalizer
- 08Tailwind CSS Circular Dial Range Input — Smart Thermostat UI
- 09Tailwind CSS Gradient Range Slider with Progress Milestones
- 10Tailwind CSS Range Slider with Pop-Up Value Bubble (Loan Calculator)
- 11Tailwind CSS Brutalist Range Slider with Thick Block Track
- 12Tailwind CSS Password Strength Meter with Range Slider Control
- 13Tailwind CSS Centre-Origin Balance Slider for Audio Pan Control
- 14Tailwind CSS Retro 8-Bit Pixel Art Range Slider (Game HP Bar)
- 15Tailwind CSS Isometric 3D Range Slider with Perspective Plane
- 16Tailwind CSS Glassmorphism Range Sliders — RGBA Colour Mixer
- 17Tailwind CSS Liquid Glass Slider with Gooey Morphing Value Bubble
Tailwind CSS Dual-Handle Price Range Slider with Live Product Count
The e-commerce workhorse: two thumbs on one track, an accent-lit segment between them, and a live result count. The port's win is geometry — the CSS original wrote four inline styles per frame and could drift; here two custom properties (--lo, --hi) drive the fill, both handles and both tooltips through calc() utilities, so the lit segment and the handles cannot disagree.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="trs-01 w-full min-h-svh grid place-items-center bg-trs01-bg font-trs01-sans px-5 py-10" id="trs-01-root" style="--lo:.1;--hi:.75">
<div class="w-[min(480px,100%)] rounded-3xl bg-trs01-card border border-trs01-edge p-7 pt-6 shadow-[0_24px_70px_-20px_oklch(0.2_0.06_250/0.55)] overflow-visible">
<header class="flex items-start justify-between gap-4 mb-7">
<div>
<h3 class="text-[17px] font-extrabold tracking-tight text-trs01-ink m-0">Price range</h3>
<p class="text-xs text-trs01-mut mt-1 m-0">Filter products by budget</p>
</div>
<span class="shrink-0 rounded-full bg-trs01-acc/12 border border-trs01-acc/35 text-trs01-acc text-[10px] font-bold tracking-[0.12em] px-2.5 py-1">E-SHOP</span>
</header>
<div class="grid grid-cols-2 gap-3 mb-6">
<div class="rounded-2xl bg-trs01-well border border-trs01-edge px-4 py-3">
<div class="text-[10px] font-semibold tracking-[0.14em] text-trs01-mut uppercase">Min</div>
<div class="font-trs01-mono text-2xl font-bold text-trs01-acc tabular-nums mt-0.5"><span class="text-sm align-super text-trs01-mut">$</span><span id="trs-01-min">20</span></div>
</div>
<div class="rounded-2xl bg-trs01-well border border-trs01-edge px-4 py-3">
<div class="text-[10px] font-semibold tracking-[0.14em] text-trs01-mut uppercase">Max</div>
<div class="font-trs01-mono text-2xl font-bold text-trs01-acc tabular-nums mt-0.5"><span class="text-sm align-super text-trs01-mut">$</span><span id="trs-01-max">150</span></div>
</div>
</div>
<div class="flex items-end gap-[3px] h-14 mb-1 px-px" id="trs-01-hist" aria-hidden="true"></div>
<div class="relative h-12 flex items-center" id="trs-01-wrap">
<div class="absolute inset-x-0 h-1.5 rounded-full bg-trs01-track"></div>
<div class="absolute h-1.5 rounded-full bg-gradient-to-r from-trs01-acc to-trs01-acc2 shadow-[0_0_14px_var(--color-trs01-acc)] left-[calc(var(--lo)*100%)] w-[calc((var(--hi)_-_var(--lo))*100%)]"></div>
<input type="range" id="trs-01-lo" min="0" max="200" value="20" aria-label="Minimum price" data-top="true"
class="peer/lo absolute inset-0 w-full h-full m-0 appearance-none bg-transparent opacity-0 pointer-events-none z-10 data-[top=true]:z-20 focus-visible:opacity-0">
<input type="range" id="trs-01-hi" min="0" max="200" value="150" aria-label="Maximum price" data-top="false"
class="peer/hi absolute inset-0 w-full h-full m-0 appearance-none bg-transparent opacity-0 pointer-events-none z-10 data-[top=true]:z-20">
<span aria-hidden="true" class="absolute top-1/2 left-[calc(var(--lo)*100%)] -translate-x-1/2 -translate-y-1/2 size-5.5 rounded-full bg-white border-[3px] border-trs01-acc shadow-[0_0_0_4px_var(--color-trs01-ring),0_4px_12px_oklch(0.15_0.05_250/0.5)] transition-transform duration-150 peer-active/lo:scale-115 peer-focus-visible/lo:ring-2 peer-focus-visible/lo:ring-white motion-reduce:transition-none">
<span class="absolute bottom-[calc(100%_+_10px)] left-1/2 -translate-x-1/2 rounded-md bg-trs01-acc px-1.5 py-0.5 font-trs01-mono text-[10px] font-bold text-trs01-bg whitespace-nowrap opacity-0 transition-opacity duration-200 peer-active/lo:opacity-100 peer-focus-visible/lo:opacity-100 motion-reduce:transition-none" id="trs-01-tip-lo">$20</span>
</span>
<span aria-hidden="true" class="absolute top-1/2 left-[calc(var(--hi)*100%)] -translate-x-1/2 -translate-y-1/2 size-5.5 rounded-full bg-white border-[3px] border-trs01-acc shadow-[0_0_0_4px_var(--color-trs01-ring),0_4px_12px_oklch(0.15_0.05_250/0.5)] transition-transform duration-150 peer-active/hi:scale-115 motion-reduce:transition-none">
<span class="absolute bottom-[calc(100%_+_10px)] left-1/2 -translate-x-1/2 rounded-md bg-trs01-acc px-1.5 py-0.5 font-trs01-mono text-[10px] font-bold text-trs01-bg whitespace-nowrap opacity-0 transition-opacity duration-200 peer-active/hi:opacity-100 motion-reduce:transition-none" id="trs-01-tip-hi">$150</span>
</span>
</div>
<div class="flex justify-between font-trs01-mono text-[10px] text-trs01-mut mb-5">
<span>$0</span><span>$50</span><span>$100</span><span>$150</span><span>$200</span>
</div>
<footer class="flex items-center justify-between gap-4 pt-5 border-t border-trs01-edge">
<p class="text-[13px] text-trs01-mut m-0" aria-live="polite"><strong class="text-trs01-acc font-bold tabular-nums" id="trs-01-count">183</strong> products in range</p>
<button type="button" class="min-h-11 rounded-xl bg-trs01-acc px-5 text-[13px] font-bold text-trs01-bg transition-[translate,box-shadow] duration-150 hover:-translate-y-px hover:shadow-[0_8px_22px_-6px_var(--color-trs01-acc)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-trs01-acc motion-reduce:hover:translate-y-0">Apply filter</button>
</footer>
</div>
</section><section class="trs-01 w-full min-h-svh grid place-items-center bg-trs01-bg font-trs01-sans px-5 py-10" id="trs-01-root" style="--lo:.1;--hi:.75">
<div class="w-[min(480px,100%)] rounded-3xl bg-trs01-card border border-trs01-edge p-7 pt-6 shadow-[0_24px_70px_-20px_oklch(0.2_0.06_250/0.55)] overflow-visible">
<header class="flex items-start justify-between gap-4 mb-7">
<div>
<h3 class="text-[17px] font-extrabold tracking-tight text-trs01-ink m-0">Price range</h3>
<p class="text-xs text-trs01-mut mt-1 m-0">Filter products by budget</p>
</div>
<span class="shrink-0 rounded-full bg-trs01-acc/12 border border-trs01-acc/35 text-trs01-acc text-[10px] font-bold tracking-[0.12em] px-2.5 py-1">E-SHOP</span>
</header>
<div class="grid grid-cols-2 gap-3 mb-6">
<div class="rounded-2xl bg-trs01-well border border-trs01-edge px-4 py-3">
<div class="text-[10px] font-semibold tracking-[0.14em] text-trs01-mut uppercase">Min</div>
<div class="font-trs01-mono text-2xl font-bold text-trs01-acc tabular-nums mt-0.5"><span class="text-sm align-super text-trs01-mut">$</span><span id="trs-01-min">20</span></div>
</div>
<div class="rounded-2xl bg-trs01-well border border-trs01-edge px-4 py-3">
<div class="text-[10px] font-semibold tracking-[0.14em] text-trs01-mut uppercase">Max</div>
<div class="font-trs01-mono text-2xl font-bold text-trs01-acc tabular-nums mt-0.5"><span class="text-sm align-super text-trs01-mut">$</span><span id="trs-01-max">150</span></div>
</div>
</div>
<div class="flex items-end gap-[3px] h-14 mb-1 px-px" id="trs-01-hist" aria-hidden="true"></div>
<div class="relative h-12 flex items-center" id="trs-01-wrap">
<div class="absolute inset-x-0 h-1.5 rounded-full bg-trs01-track"></div>
<div class="absolute h-1.5 rounded-full bg-gradient-to-r from-trs01-acc to-trs01-acc2 shadow-[0_0_14px_var(--color-trs01-acc)] left-[calc(var(--lo)*100%)] w-[calc((var(--hi)_-_var(--lo))*100%)]"></div>
<input type="range" id="trs-01-lo" min="0" max="200" value="20" aria-label="Minimum price" data-top="true"
class="peer/lo absolute inset-0 w-full h-full m-0 appearance-none bg-transparent opacity-0 pointer-events-none z-10 data-[top=true]:z-20 focus-visible:opacity-0">
<input type="range" id="trs-01-hi" min="0" max="200" value="150" aria-label="Maximum price" data-top="false"
class="peer/hi absolute inset-0 w-full h-full m-0 appearance-none bg-transparent opacity-0 pointer-events-none z-10 data-[top=true]:z-20">
<span aria-hidden="true" class="absolute top-1/2 left-[calc(var(--lo)*100%)] -translate-x-1/2 -translate-y-1/2 size-5.5 rounded-full bg-white border-[3px] border-trs01-acc shadow-[0_0_0_4px_var(--color-trs01-ring),0_4px_12px_oklch(0.15_0.05_250/0.5)] transition-transform duration-150 peer-active/lo:scale-115 peer-focus-visible/lo:ring-2 peer-focus-visible/lo:ring-white motion-reduce:transition-none">
<span class="absolute bottom-[calc(100%_+_10px)] left-1/2 -translate-x-1/2 rounded-md bg-trs01-acc px-1.5 py-0.5 font-trs01-mono text-[10px] font-bold text-trs01-bg whitespace-nowrap opacity-0 transition-opacity duration-200 peer-active/lo:opacity-100 peer-focus-visible/lo:opacity-100 motion-reduce:transition-none" id="trs-01-tip-lo">$20</span>
</span>
<span aria-hidden="true" class="absolute top-1/2 left-[calc(var(--hi)*100%)] -translate-x-1/2 -translate-y-1/2 size-5.5 rounded-full bg-white border-[3px] border-trs01-acc shadow-[0_0_0_4px_var(--color-trs01-ring),0_4px_12px_oklch(0.15_0.05_250/0.5)] transition-transform duration-150 peer-active/hi:scale-115 motion-reduce:transition-none">
<span class="absolute bottom-[calc(100%_+_10px)] left-1/2 -translate-x-1/2 rounded-md bg-trs01-acc px-1.5 py-0.5 font-trs01-mono text-[10px] font-bold text-trs01-bg whitespace-nowrap opacity-0 transition-opacity duration-200 peer-active/hi:opacity-100 motion-reduce:transition-none" id="trs-01-tip-hi">$150</span>
</span>
</div>
<div class="flex justify-between font-trs01-mono text-[10px] text-trs01-mut mb-5">
<span>$0</span><span>$50</span><span>$100</span><span>$150</span><span>$200</span>
</div>
<footer class="flex items-center justify-between gap-4 pt-5 border-t border-trs01-edge">
<p class="text-[13px] text-trs01-mut m-0" aria-live="polite"><strong class="text-trs01-acc font-bold tabular-nums" id="trs-01-count">183</strong> products in range</p>
<button type="button" class="min-h-11 rounded-xl bg-trs01-acc px-5 text-[13px] font-bold text-trs01-bg transition-[translate,box-shadow] duration-150 hover:-translate-y-px hover:shadow-[0_8px_22px_-6px_var(--color-trs01-acc)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-trs01-acc motion-reduce:hover:translate-y-0">Apply filter</button>
</footer>
</div>
</section>@import "tailwindcss";
@theme {
--color-trs01-bg: #0a1628;
--color-trs01-bg: oklch(0.2 0.045 255);
--color-trs01-card: #142042;
--color-trs01-card: oklch(0.26 0.055 262);
--color-trs01-well: #18264a;
--color-trs01-well: oklch(0.29 0.055 262);
--color-trs01-edge: #1f3159;
--color-trs01-edge: oklch(0.34 0.06 262);
--color-trs01-track: #22365f;
--color-trs01-track: oklch(0.36 0.055 262);
--color-trs01-acc: #00c896;
--color-trs01-acc: oklch(0.75 0.16 165);
--color-trs01-acc2: #00e5b0;
--color-trs01-acc2: oklch(0.84 0.15 168);
--color-trs01-ring: #0d3b34;
--color-trs01-ring: oklch(0.36 0.07 168);
--color-trs01-ink: #e8f0fe;
--color-trs01-ink: oklch(0.95 0.015 255);
--color-trs01-mut: #7d94ba;
--color-trs01-mut: oklch(0.66 0.045 255);
--font-trs01-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-trs01-mono: 'JetBrains Mono', ui-monospace, monospace;
}
/* Registered so the calc()-driven fill and handles interpolate instead of jumping. */
@property --lo {
syntax: "<number>";
inherits: true;
initial-value: 0;
}
@property --hi {
syntax: "<number>";
inherits: true;
initial-value: 1;
}
.trs-01 {
width: 100%;
min-height: 100svh;
transition: --lo 60ms linear, --hi 60ms linear;
}
@media (prefers-reduced-motion: reduce) {
.trs-01 {
transition: none;
}
}
/* The one thing utilities cannot say: the native thumbs are invisible hit targets
layered over the utility-styled handles, and they alone take pointer events. */
.trs-01 input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 26px;
height: 26px;
border-radius: 9999px;
background: transparent;
pointer-events: all;
cursor: grab;
}
.trs-01 input[type="range"]:active::-webkit-slider-thumb {
cursor: grabbing;
}
.trs-01 input[type="range"]::-moz-range-thumb {
width: 26px;
height: 26px;
border: 0;
border-radius: 9999px;
background: transparent;
pointer-events: all;
cursor: grab;
}@import "tailwindcss";
@theme {
--color-trs01-bg: #0a1628;
--color-trs01-bg: oklch(0.2 0.045 255);
--color-trs01-card: #142042;
--color-trs01-card: oklch(0.26 0.055 262);
--color-trs01-well: #18264a;
--color-trs01-well: oklch(0.29 0.055 262);
--color-trs01-edge: #1f3159;
--color-trs01-edge: oklch(0.34 0.06 262);
--color-trs01-track: #22365f;
--color-trs01-track: oklch(0.36 0.055 262);
--color-trs01-acc: #00c896;
--color-trs01-acc: oklch(0.75 0.16 165);
--color-trs01-acc2: #00e5b0;
--color-trs01-acc2: oklch(0.84 0.15 168);
--color-trs01-ring: #0d3b34;
--color-trs01-ring: oklch(0.36 0.07 168);
--color-trs01-ink: #e8f0fe;
--color-trs01-ink: oklch(0.95 0.015 255);
--color-trs01-mut: #7d94ba;
--color-trs01-mut: oklch(0.66 0.045 255);
--font-trs01-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-trs01-mono: 'JetBrains Mono', ui-monospace, monospace;
}
/* Registered so the calc()-driven fill and handles interpolate instead of jumping. */
@property --lo {
syntax: "<number>";
inherits: true;
initial-value: 0;
}
@property --hi {
syntax: "<number>";
inherits: true;
initial-value: 1;
}
.trs-01 {
width: 100%;
min-height: 100svh;
transition: --lo 60ms linear, --hi 60ms linear;
}
@media (prefers-reduced-motion: reduce) {
.trs-01 {
transition: none;
}
}
/* The one thing utilities cannot say: the native thumbs are invisible hit targets
layered over the utility-styled handles, and they alone take pointer events. */
.trs-01 input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 26px;
height: 26px;
border-radius: 9999px;
background: transparent;
pointer-events: all;
cursor: grab;
}
.trs-01 input[type="range"]:active::-webkit-slider-thumb {
cursor: grabbing;
}
.trs-01 input[type="range"]::-moz-range-thumb {
width: 26px;
height: 26px;
border: 0;
border-radius: 9999px;
background: transparent;
pointer-events: all;
cursor: grab;
}(function(){
var root = document.getElementById('trs-01-root');
var lo = document.getElementById('trs-01-lo'), hi = document.getElementById('trs-01-hi');
var hist = document.getElementById('trs-01-hist');
var GAP = 10, TOTAL = 247, BARS = 24;
var shape = [12,18,26,38,52,68,84,96,100,92,80,70,62,54,46,40,34,28,23,19,15,12,9,7];
shape.forEach(function(h){
var b = document.createElement('span');
b.className = 'flex-1 rounded-sm bg-trs01-track transition-colors duration-200 data-[in=true]:bg-trs01-acc/70 motion-reduce:transition-none';
b.style.height = Math.max(6, h * 0.5) + 'px';
b.dataset.in = 'false';
hist.appendChild(b);
});
var bars = Array.prototype.slice.call(hist.children);
function frac(v){ return (v - +lo.min) / (+lo.max - +lo.min); }
function update(){
var a = Math.min(+lo.value, +hi.value), b = Math.max(+lo.value, +hi.value);
if (b - a < GAP) { if (document.activeElement === lo) { a = Math.min(b - GAP, a); lo.value = a; } else { b = a + GAP; hi.value = b; } }
root.style.setProperty('--lo', frac(a));
root.style.setProperty('--hi', frac(b));
document.getElementById('trs-01-min').textContent = a;
document.getElementById('trs-01-max').textContent = b;
document.getElementById('trs-01-tip-lo').textContent = '$' + a;
document.getElementById('trs-01-tip-hi').textContent = '$' + b;
lo.setAttribute('aria-valuetext', '$' + a + ' minimum');
hi.setAttribute('aria-valuetext', '$' + b + ' maximum');
bars.forEach(function(bar, i){
var c = (i + 0.5) / BARS;
bar.dataset.in = (c >= frac(a) && c <= frac(b)) ? 'true' : 'false';
});
document.getElementById('trs-01-count').textContent = Math.max(3, Math.round(TOTAL * (b - a) / 200));
}
[lo, hi].forEach(function(inp){
inp.addEventListener('input', function(){
lo.dataset.top = String(inp === lo);
hi.dataset.top = String(inp === hi);
update();
});
inp.addEventListener('pointerdown', function(){
lo.dataset.top = String(inp === lo);
hi.dataset.top = String(inp === hi);
});
});
update();
})();(function(){
var root = document.getElementById('trs-01-root');
var lo = document.getElementById('trs-01-lo'), hi = document.getElementById('trs-01-hi');
var hist = document.getElementById('trs-01-hist');
var GAP = 10, TOTAL = 247, BARS = 24;
var shape = [12,18,26,38,52,68,84,96,100,92,80,70,62,54,46,40,34,28,23,19,15,12,9,7];
shape.forEach(function(h){
var b = document.createElement('span');
b.className = 'flex-1 rounded-sm bg-trs01-track transition-colors duration-200 data-[in=true]:bg-trs01-acc/70 motion-reduce:transition-none';
b.style.height = Math.max(6, h * 0.5) + 'px';
b.dataset.in = 'false';
hist.appendChild(b);
});
var bars = Array.prototype.slice.call(hist.children);
function frac(v){ return (v - +lo.min) / (+lo.max - +lo.min); }
function update(){
var a = Math.min(+lo.value, +hi.value), b = Math.max(+lo.value, +hi.value);
if (b - a < GAP) { if (document.activeElement === lo) { a = Math.min(b - GAP, a); lo.value = a; } else { b = a + GAP; hi.value = b; } }
root.style.setProperty('--lo', frac(a));
root.style.setProperty('--hi', frac(b));
document.getElementById('trs-01-min').textContent = a;
document.getElementById('trs-01-max').textContent = b;
document.getElementById('trs-01-tip-lo').textContent = '$' + a;
document.getElementById('trs-01-tip-hi').textContent = '$' + b;
lo.setAttribute('aria-valuetext', '$' + a + ' minimum');
hi.setAttribute('aria-valuetext', '$' + b + ' maximum');
bars.forEach(function(bar, i){
var c = (i + 0.5) / BARS;
bar.dataset.in = (c >= frac(a) && c <= frac(b)) ? 'true' : 'false';
});
document.getElementById('trs-01-count').textContent = Math.max(3, Math.round(TOTAL * (b - a) / 200));
}
[lo, hi].forEach(function(inp){
inp.addEventListener('input', function(){
lo.dataset.top = String(inp === lo);
hi.dataset.top = String(inp === hi);
update();
});
inp.addEventListener('pointerdown', function(){
lo.dataset.top = String(inp === lo);
hi.dataset.top = String(inp === hi);
});
});
update();
})();Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>Compiles in the browser — perfect for a CodePen or a quick test, not for production.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@theme {
--color-trs01-bg: #0a1628;
--color-trs01-bg: oklch(0.2 0.045 255);
--color-trs01-card: #142042;
--color-trs01-card: oklch(0.26 0.055 262);
--color-trs01-well: #18264a;
--color-trs01-well: oklch(0.29 0.055 262);
--color-trs01-edge: #1f3159;
--color-trs01-edge: oklch(0.34 0.06 262);
--color-trs01-track: #22365f;
--color-trs01-track: oklch(0.36 0.055 262);
--color-trs01-acc: #00c896;
--color-trs01-acc: oklch(0.75 0.16 165);
--color-trs01-acc2: #00e5b0;
--color-trs01-acc2: oklch(0.84 0.15 168);
--color-trs01-ring: #0d3b34;
--color-trs01-ring: oklch(0.36 0.07 168);
--color-trs01-ink: #e8f0fe;
--color-trs01-ink: oklch(0.95 0.015 255);
--color-trs01-mut: #7d94ba;
--color-trs01-mut: oklch(0.66 0.045 255);
--font-trs01-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-trs01-mono: 'JetBrains Mono', ui-monospace, monospace;
}
/* Registered so the calc()-driven fill and handles interpolate instead of jumping. */
@property --lo {
syntax: "<number>";
inherits: true;
initial-value: 0;
}
@property --hi {
syntax: "<number>";
inherits: true;
initial-value: 1;
}
.trs-01 {
width: 100%;
min-height: 100svh;
transition: --lo 60ms linear, --hi 60ms linear;
}
@media (prefers-reduced-motion: reduce) {
.trs-01 {
transition: none;
}
}
/* The one thing utilities cannot say: the native thumbs are invisible hit targets
layered over the utility-styled handles, and they alone take pointer events. */
.trs-01 input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 26px;
height: 26px;
border-radius: 9999px;
background: transparent;
pointer-events: all;
cursor: grab;
}
.trs-01 input[type="range"]:active::-webkit-slider-thumb {
cursor: grabbing;
}
.trs-01 input[type="range"]::-moz-range-thumb {
width: 26px;
height: 26px;
border: 0;
border-radius: 9999px;
background: transparent;
pointer-events: all;
cursor: grab;
}@import "tailwindcss";
@theme {
--color-trs01-bg: #0a1628;
--color-trs01-bg: oklch(0.2 0.045 255);
--color-trs01-card: #142042;
--color-trs01-card: oklch(0.26 0.055 262);
--color-trs01-well: #18264a;
--color-trs01-well: oklch(0.29 0.055 262);
--color-trs01-edge: #1f3159;
--color-trs01-edge: oklch(0.34 0.06 262);
--color-trs01-track: #22365f;
--color-trs01-track: oklch(0.36 0.055 262);
--color-trs01-acc: #00c896;
--color-trs01-acc: oklch(0.75 0.16 165);
--color-trs01-acc2: #00e5b0;
--color-trs01-acc2: oklch(0.84 0.15 168);
--color-trs01-ring: #0d3b34;
--color-trs01-ring: oklch(0.36 0.07 168);
--color-trs01-ink: #e8f0fe;
--color-trs01-ink: oklch(0.95 0.015 255);
--color-trs01-mut: #7d94ba;
--color-trs01-mut: oklch(0.66 0.045 255);
--font-trs01-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-trs01-mono: 'JetBrains Mono', ui-monospace, monospace;
}
/* Registered so the calc()-driven fill and handles interpolate instead of jumping. */
@property --lo {
syntax: "<number>";
inherits: true;
initial-value: 0;
}
@property --hi {
syntax: "<number>";
inherits: true;
initial-value: 1;
}
.trs-01 {
width: 100%;
min-height: 100svh;
transition: --lo 60ms linear, --hi 60ms linear;
}
@media (prefers-reduced-motion: reduce) {
.trs-01 {
transition: none;
}
}
/* The one thing utilities cannot say: the native thumbs are invisible hit targets
layered over the utility-styled handles, and they alone take pointer events. */
.trs-01 input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 26px;
height: 26px;
border-radius: 9999px;
background: transparent;
pointer-events: all;
cursor: grab;
}
.trs-01 input[type="range"]:active::-webkit-slider-thumb {
cursor: grabbing;
}
.trs-01 input[type="range"]::-moz-range-thumb {
width: 26px;
height: 26px;
border: 0;
border-radius: 9999px;
background: transparent;
pointer-events: all;
cursor: grab;
}<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@500;700&display=swap">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@500;700&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- Four inline style writes per frame become one pair of custom-property writes. The fill, both handles and both tooltips read
--lo/--hithroughcalc()utilities, so they cannot drift out of sync. peer-active/lo:replaces themousedown/touchstart/mouseup/touchendclass-toggle quartet the CSS version needed — four listeners deleted, and it now works for keyboard focus too.- UI upgrade: a 24-bar price histogram sits behind the track and recolours in range, so the filter shows where the stock is; the handles also got a focus ring and the count row an
aria-liveannouncement.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind CSS Dual-Handle Price Range Slider with Live Product Count
Source: https://codefronts.com/components/tailwind-css-range-sliders/tailwind-dual-handle-price-range-slider/
The e-commerce workhorse: two thumbs on one track, an accent-lit segment between them, and a live result count. The port's win is geometry — the CSS original wrote four inline styles per frame and could drift; here two custom properties (--lo, --hi) drive the fill, both handles and both tooltips through calc() utilities, so the lit segment and the handles cannot disagree.
## HTML
```html
<section class="trs-01 w-full min-h-svh grid place-items-center bg-trs01-bg font-trs01-sans px-5 py-10" id="trs-01-root" style="--lo:.1;--hi:.75">
<div class="w-[min(480px,100%)] rounded-3xl bg-trs01-card border border-trs01-edge p-7 pt-6 shadow-[0_24px_70px_-20px_oklch(0.2_0.06_250/0.55)] overflow-visible">
<header class="flex items-start justify-between gap-4 mb-7">
<div>
<h3 class="text-[17px] font-extrabold tracking-tight text-trs01-ink m-0">Price range</h3>
<p class="text-xs text-trs01-mut mt-1 m-0">Filter products by budget</p>
</div>
<span class="shrink-0 rounded-full bg-trs01-acc/12 border border-trs01-acc/35 text-trs01-acc text-[10px] font-bold tracking-[0.12em] px-2.5 py-1">E-SHOP</span>
</header>
<div class="grid grid-cols-2 gap-3 mb-6">
<div class="rounded-2xl bg-trs01-well border border-trs01-edge px-4 py-3">
<div class="text-[10px] font-semibold tracking-[0.14em] text-trs01-mut uppercase">Min</div>
<div class="font-trs01-mono text-2xl font-bold text-trs01-acc tabular-nums mt-0.5"><span class="text-sm align-super text-trs01-mut">$</span><span id="trs-01-min">20</span></div>
</div>
<div class="rounded-2xl bg-trs01-well border border-trs01-edge px-4 py-3">
<div class="text-[10px] font-semibold tracking-[0.14em] text-trs01-mut uppercase">Max</div>
<div class="font-trs01-mono text-2xl font-bold text-trs01-acc tabular-nums mt-0.5"><span class="text-sm align-super text-trs01-mut">$</span><span id="trs-01-max">150</span></div>
</div>
</div>
<div class="flex items-end gap-[3px] h-14 mb-1 px-px" id="trs-01-hist" aria-hidden="true"></div>
<div class="relative h-12 flex items-center" id="trs-01-wrap">
<div class="absolute inset-x-0 h-1.5 rounded-full bg-trs01-track"></div>
<div class="absolute h-1.5 rounded-full bg-gradient-to-r from-trs01-acc to-trs01-acc2 shadow-[0_0_14px_var(--color-trs01-acc)] left-[calc(var(--lo)*100%)] w-[calc((var(--hi)_-_var(--lo))*100%)]"></div>
<input type="range" id="trs-01-lo" min="0" max="200" value="20" aria-label="Minimum price" data-top="true"
class="peer/lo absolute inset-0 w-full h-full m-0 appearance-none bg-transparent opacity-0 pointer-events-none z-10 data-[top=true]:z-20 focus-visible:opacity-0">
<input type="range" id="trs-01-hi" min="0" max="200" value="150" aria-label="Maximum price" data-top="false"
class="peer/hi absolute inset-0 w-full h-full m-0 appearance-none bg-transparent opacity-0 pointer-events-none z-10 data-[top=true]:z-20">
<span aria-hidden="true" class="absolute top-1/2 left-[calc(var(--lo)*100%)] -translate-x-1/2 -translate-y-1/2 size-5.5 rounded-full bg-white border-[3px] border-trs01-acc shadow-[0_0_0_4px_var(--color-trs01-ring),0_4px_12px_oklch(0.15_0.05_250/0.5)] transition-transform duration-150 peer-active/lo:scale-115 peer-focus-visible/lo:ring-2 peer-focus-visible/lo:ring-white motion-reduce:transition-none">
<span class="absolute bottom-[calc(100%_+_10px)] left-1/2 -translate-x-1/2 rounded-md bg-trs01-acc px-1.5 py-0.5 font-trs01-mono text-[10px] font-bold text-trs01-bg whitespace-nowrap opacity-0 transition-opacity duration-200 peer-active/lo:opacity-100 peer-focus-visible/lo:opacity-100 motion-reduce:transition-none" id="trs-01-tip-lo">$20</span>
</span>
<span aria-hidden="true" class="absolute top-1/2 left-[calc(var(--hi)*100%)] -translate-x-1/2 -translate-y-1/2 size-5.5 rounded-full bg-white border-[3px] border-trs01-acc shadow-[0_0_0_4px_var(--color-trs01-ring),0_4px_12px_oklch(0.15_0.05_250/0.5)] transition-transform duration-150 peer-active/hi:scale-115 motion-reduce:transition-none">
<span class="absolute bottom-[calc(100%_+_10px)] left-1/2 -translate-x-1/2 rounded-md bg-trs01-acc px-1.5 py-0.5 font-trs01-mono text-[10px] font-bold text-trs01-bg whitespace-nowrap opacity-0 transition-opacity duration-200 peer-active/hi:opacity-100 motion-reduce:transition-none" id="trs-01-tip-hi">$150</span>
</span>
</div>
<div class="flex justify-between font-trs01-mono text-[10px] text-trs01-mut mb-5">
<span>$0</span><span>$50</span><span>$100</span><span>$150</span><span>$200</span>
</div>
<footer class="flex items-center justify-between gap-4 pt-5 border-t border-trs01-edge">
<p class="text-[13px] text-trs01-mut m-0" aria-live="polite"><strong class="text-trs01-acc font-bold tabular-nums" id="trs-01-count">183</strong> products in range</p>
<button type="button" class="min-h-11 rounded-xl bg-trs01-acc px-5 text-[13px] font-bold text-trs01-bg transition-[translate,box-shadow] duration-150 hover:-translate-y-px hover:shadow-[0_8px_22px_-6px_var(--color-trs01-acc)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-trs01-acc motion-reduce:hover:translate-y-0">Apply filter</button>
</footer>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-trs01-bg: #0a1628;
--color-trs01-bg: oklch(0.2 0.045 255);
--color-trs01-card: #142042;
--color-trs01-card: oklch(0.26 0.055 262);
--color-trs01-well: #18264a;
--color-trs01-well: oklch(0.29 0.055 262);
--color-trs01-edge: #1f3159;
--color-trs01-edge: oklch(0.34 0.06 262);
--color-trs01-track: #22365f;
--color-trs01-track: oklch(0.36 0.055 262);
--color-trs01-acc: #00c896;
--color-trs01-acc: oklch(0.75 0.16 165);
--color-trs01-acc2: #00e5b0;
--color-trs01-acc2: oklch(0.84 0.15 168);
--color-trs01-ring: #0d3b34;
--color-trs01-ring: oklch(0.36 0.07 168);
--color-trs01-ink: #e8f0fe;
--color-trs01-ink: oklch(0.95 0.015 255);
--color-trs01-mut: #7d94ba;
--color-trs01-mut: oklch(0.66 0.045 255);
--font-trs01-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-trs01-mono: 'JetBrains Mono', ui-monospace, monospace;
}
/* Registered so the calc()-driven fill and handles interpolate instead of jumping. */
@property --lo {
syntax: "<number>";
inherits: true;
initial-value: 0;
}
@property --hi {
syntax: "<number>";
inherits: true;
initial-value: 1;
}
.trs-01 {
width: 100%;
min-height: 100svh;
transition: --lo 60ms linear, --hi 60ms linear;
}
@media (prefers-reduced-motion: reduce) {
.trs-01 {
transition: none;
}
}
/* The one thing utilities cannot say: the native thumbs are invisible hit targets
layered over the utility-styled handles, and they alone take pointer events. */
.trs-01 input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 26px;
height: 26px;
border-radius: 9999px;
background: transparent;
pointer-events: all;
cursor: grab;
}
.trs-01 input[type="range"]:active::-webkit-slider-thumb {
cursor: grabbing;
}
.trs-01 input[type="range"]::-moz-range-thumb {
width: 26px;
height: 26px;
border: 0;
border-radius: 9999px;
background: transparent;
pointer-events: all;
cursor: grab;
}
```
## JavaScript
```js
(function(){
var root = document.getElementById('trs-01-root');
var lo = document.getElementById('trs-01-lo'), hi = document.getElementById('trs-01-hi');
var hist = document.getElementById('trs-01-hist');
var GAP = 10, TOTAL = 247, BARS = 24;
var shape = [12,18,26,38,52,68,84,96,100,92,80,70,62,54,46,40,34,28,23,19,15,12,9,7];
shape.forEach(function(h){
var b = document.createElement('span');
b.className = 'flex-1 rounded-sm bg-trs01-track transition-colors duration-200 data-[in=true]:bg-trs01-acc/70 motion-reduce:transition-none';
b.style.height = Math.max(6, h * 0.5) + 'px';
b.dataset.in = 'false';
hist.appendChild(b);
});
var bars = Array.prototype.slice.call(hist.children);
function frac(v){ return (v - +lo.min) / (+lo.max - +lo.min); }
function update(){
var a = Math.min(+lo.value, +hi.value), b = Math.max(+lo.value, +hi.value);
if (b - a < GAP) { if (document.activeElement === lo) { a = Math.min(b - GAP, a); lo.value = a; } else { b = a + GAP; hi.value = b; } }
root.style.setProperty('--lo', frac(a));
root.style.setProperty('--hi', frac(b));
document.getElementById('trs-01-min').textContent = a;
document.getElementById('trs-01-max').textContent = b;
document.getElementById('trs-01-tip-lo').textContent = '$' + a;
document.getElementById('trs-01-tip-hi').textContent = '$' + b;
lo.setAttribute('aria-valuetext', '$' + a + ' minimum');
hi.setAttribute('aria-valuetext', '$' + b + ' maximum');
bars.forEach(function(bar, i){
var c = (i + 0.5) / BARS;
bar.dataset.in = (c >= frac(a) && c <= frac(b)) ? 'true' : 'false';
});
document.getElementById('trs-01-count').textContent = Math.max(3, Math.round(TOTAL * (b - a) / 200));
}
[lo, hi].forEach(function(inp){
inp.addEventListener('input', function(){
lo.dataset.top = String(inp === lo);
hi.dataset.top = String(inp === hi);
update();
});
inp.addEventListener('pointerdown', function(){
lo.dataset.top = String(inp === lo);
hi.dataset.top = String(inp === hi);
});
});
update();
})();
```Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind CSS Dual-Handle Price Range Slider with Live Product Count
Source: https://codefronts.com/components/tailwind-css-range-sliders/tailwind-dual-handle-price-range-slider/
The e-commerce workhorse: two thumbs on one track, an accent-lit segment between them, and a live result count. The port's win is geometry — the CSS original wrote four inline styles per frame and could drift; here two custom properties (--lo, --hi) drive the fill, both handles and both tooltips through calc() utilities, so the lit segment and the handles cannot disagree.
## HTML
```html
<section class="trs-01 w-full min-h-svh grid place-items-center bg-trs01-bg font-trs01-sans px-5 py-10" id="trs-01-root" style="--lo:.1;--hi:.75">
<div class="w-[min(480px,100%)] rounded-3xl bg-trs01-card border border-trs01-edge p-7 pt-6 shadow-[0_24px_70px_-20px_oklch(0.2_0.06_250/0.55)] overflow-visible">
<header class="flex items-start justify-between gap-4 mb-7">
<div>
<h3 class="text-[17px] font-extrabold tracking-tight text-trs01-ink m-0">Price range</h3>
<p class="text-xs text-trs01-mut mt-1 m-0">Filter products by budget</p>
</div>
<span class="shrink-0 rounded-full bg-trs01-acc/12 border border-trs01-acc/35 text-trs01-acc text-[10px] font-bold tracking-[0.12em] px-2.5 py-1">E-SHOP</span>
</header>
<div class="grid grid-cols-2 gap-3 mb-6">
<div class="rounded-2xl bg-trs01-well border border-trs01-edge px-4 py-3">
<div class="text-[10px] font-semibold tracking-[0.14em] text-trs01-mut uppercase">Min</div>
<div class="font-trs01-mono text-2xl font-bold text-trs01-acc tabular-nums mt-0.5"><span class="text-sm align-super text-trs01-mut">$</span><span id="trs-01-min">20</span></div>
</div>
<div class="rounded-2xl bg-trs01-well border border-trs01-edge px-4 py-3">
<div class="text-[10px] font-semibold tracking-[0.14em] text-trs01-mut uppercase">Max</div>
<div class="font-trs01-mono text-2xl font-bold text-trs01-acc tabular-nums mt-0.5"><span class="text-sm align-super text-trs01-mut">$</span><span id="trs-01-max">150</span></div>
</div>
</div>
<div class="flex items-end gap-[3px] h-14 mb-1 px-px" id="trs-01-hist" aria-hidden="true"></div>
<div class="relative h-12 flex items-center" id="trs-01-wrap">
<div class="absolute inset-x-0 h-1.5 rounded-full bg-trs01-track"></div>
<div class="absolute h-1.5 rounded-full bg-gradient-to-r from-trs01-acc to-trs01-acc2 shadow-[0_0_14px_var(--color-trs01-acc)] left-[calc(var(--lo)*100%)] w-[calc((var(--hi)_-_var(--lo))*100%)]"></div>
<input type="range" id="trs-01-lo" min="0" max="200" value="20" aria-label="Minimum price" data-top="true"
class="peer/lo absolute inset-0 w-full h-full m-0 appearance-none bg-transparent opacity-0 pointer-events-none z-10 data-[top=true]:z-20 focus-visible:opacity-0">
<input type="range" id="trs-01-hi" min="0" max="200" value="150" aria-label="Maximum price" data-top="false"
class="peer/hi absolute inset-0 w-full h-full m-0 appearance-none bg-transparent opacity-0 pointer-events-none z-10 data-[top=true]:z-20">
<span aria-hidden="true" class="absolute top-1/2 left-[calc(var(--lo)*100%)] -translate-x-1/2 -translate-y-1/2 size-5.5 rounded-full bg-white border-[3px] border-trs01-acc shadow-[0_0_0_4px_var(--color-trs01-ring),0_4px_12px_oklch(0.15_0.05_250/0.5)] transition-transform duration-150 peer-active/lo:scale-115 peer-focus-visible/lo:ring-2 peer-focus-visible/lo:ring-white motion-reduce:transition-none">
<span class="absolute bottom-[calc(100%_+_10px)] left-1/2 -translate-x-1/2 rounded-md bg-trs01-acc px-1.5 py-0.5 font-trs01-mono text-[10px] font-bold text-trs01-bg whitespace-nowrap opacity-0 transition-opacity duration-200 peer-active/lo:opacity-100 peer-focus-visible/lo:opacity-100 motion-reduce:transition-none" id="trs-01-tip-lo">$20</span>
</span>
<span aria-hidden="true" class="absolute top-1/2 left-[calc(var(--hi)*100%)] -translate-x-1/2 -translate-y-1/2 size-5.5 rounded-full bg-white border-[3px] border-trs01-acc shadow-[0_0_0_4px_var(--color-trs01-ring),0_4px_12px_oklch(0.15_0.05_250/0.5)] transition-transform duration-150 peer-active/hi:scale-115 motion-reduce:transition-none">
<span class="absolute bottom-[calc(100%_+_10px)] left-1/2 -translate-x-1/2 rounded-md bg-trs01-acc px-1.5 py-0.5 font-trs01-mono text-[10px] font-bold text-trs01-bg whitespace-nowrap opacity-0 transition-opacity duration-200 peer-active/hi:opacity-100 motion-reduce:transition-none" id="trs-01-tip-hi">$150</span>
</span>
</div>
<div class="flex justify-between font-trs01-mono text-[10px] text-trs01-mut mb-5">
<span>$0</span><span>$50</span><span>$100</span><span>$150</span><span>$200</span>
</div>
<footer class="flex items-center justify-between gap-4 pt-5 border-t border-trs01-edge">
<p class="text-[13px] text-trs01-mut m-0" aria-live="polite"><strong class="text-trs01-acc font-bold tabular-nums" id="trs-01-count">183</strong> products in range</p>
<button type="button" class="min-h-11 rounded-xl bg-trs01-acc px-5 text-[13px] font-bold text-trs01-bg transition-[translate,box-shadow] duration-150 hover:-translate-y-px hover:shadow-[0_8px_22px_-6px_var(--color-trs01-acc)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-trs01-acc motion-reduce:hover:translate-y-0">Apply filter</button>
</footer>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-trs01-bg: #0a1628;
--color-trs01-bg: oklch(0.2 0.045 255);
--color-trs01-card: #142042;
--color-trs01-card: oklch(0.26 0.055 262);
--color-trs01-well: #18264a;
--color-trs01-well: oklch(0.29 0.055 262);
--color-trs01-edge: #1f3159;
--color-trs01-edge: oklch(0.34 0.06 262);
--color-trs01-track: #22365f;
--color-trs01-track: oklch(0.36 0.055 262);
--color-trs01-acc: #00c896;
--color-trs01-acc: oklch(0.75 0.16 165);
--color-trs01-acc2: #00e5b0;
--color-trs01-acc2: oklch(0.84 0.15 168);
--color-trs01-ring: #0d3b34;
--color-trs01-ring: oklch(0.36 0.07 168);
--color-trs01-ink: #e8f0fe;
--color-trs01-ink: oklch(0.95 0.015 255);
--color-trs01-mut: #7d94ba;
--color-trs01-mut: oklch(0.66 0.045 255);
--font-trs01-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-trs01-mono: 'JetBrains Mono', ui-monospace, monospace;
}
/* Registered so the calc()-driven fill and handles interpolate instead of jumping. */
@property --lo {
syntax: "<number>";
inherits: true;
initial-value: 0;
}
@property --hi {
syntax: "<number>";
inherits: true;
initial-value: 1;
}
.trs-01 {
width: 100%;
min-height: 100svh;
transition: --lo 60ms linear, --hi 60ms linear;
}
@media (prefers-reduced-motion: reduce) {
.trs-01 {
transition: none;
}
}
/* The one thing utilities cannot say: the native thumbs are invisible hit targets
layered over the utility-styled handles, and they alone take pointer events. */
.trs-01 input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 26px;
height: 26px;
border-radius: 9999px;
background: transparent;
pointer-events: all;
cursor: grab;
}
.trs-01 input[type="range"]:active::-webkit-slider-thumb {
cursor: grabbing;
}
.trs-01 input[type="range"]::-moz-range-thumb {
width: 26px;
height: 26px;
border: 0;
border-radius: 9999px;
background: transparent;
pointer-events: all;
cursor: grab;
}
```
## JavaScript
```js
(function(){
var root = document.getElementById('trs-01-root');
var lo = document.getElementById('trs-01-lo'), hi = document.getElementById('trs-01-hi');
var hist = document.getElementById('trs-01-hist');
var GAP = 10, TOTAL = 247, BARS = 24;
var shape = [12,18,26,38,52,68,84,96,100,92,80,70,62,54,46,40,34,28,23,19,15,12,9,7];
shape.forEach(function(h){
var b = document.createElement('span');
b.className = 'flex-1 rounded-sm bg-trs01-track transition-colors duration-200 data-[in=true]:bg-trs01-acc/70 motion-reduce:transition-none';
b.style.height = Math.max(6, h * 0.5) + 'px';
b.dataset.in = 'false';
hist.appendChild(b);
});
var bars = Array.prototype.slice.call(hist.children);
function frac(v){ return (v - +lo.min) / (+lo.max - +lo.min); }
function update(){
var a = Math.min(+lo.value, +hi.value), b = Math.max(+lo.value, +hi.value);
if (b - a < GAP) { if (document.activeElement === lo) { a = Math.min(b - GAP, a); lo.value = a; } else { b = a + GAP; hi.value = b; } }
root.style.setProperty('--lo', frac(a));
root.style.setProperty('--hi', frac(b));
document.getElementById('trs-01-min').textContent = a;
document.getElementById('trs-01-max').textContent = b;
document.getElementById('trs-01-tip-lo').textContent = '$' + a;
document.getElementById('trs-01-tip-hi').textContent = '$' + b;
lo.setAttribute('aria-valuetext', '$' + a + ' minimum');
hi.setAttribute('aria-valuetext', '$' + b + ' maximum');
bars.forEach(function(bar, i){
var c = (i + 0.5) / BARS;
bar.dataset.in = (c >= frac(a) && c <= frac(b)) ? 'true' : 'false';
});
document.getElementById('trs-01-count').textContent = Math.max(3, Math.round(TOTAL * (b - a) / 200));
}
[lo, hi].forEach(function(inp){
inp.addEventListener('input', function(){
lo.dataset.top = String(inp === lo);
hi.dataset.top = String(inp === hi);
update();
});
inp.addEventListener('pointerdown', function(){
lo.dataset.top = String(inp === lo);
hi.dataset.top = String(inp === hi);
});
});
update();
})();
```How this works
Two transparent inputs, two utility handles. Both natives are opacity-0 with pointer-events-none, and only their thumbs take pointer events (a two-line ::-webkit-slider-thumb shim, the one thing utilities cannot say). The visible handles are <span>s positioned with left-[calc(var(--lo)*100%)], which is what lets them carry a ring, a shadow and a tooltip.
The z-index swap is real work, not a workaround. The controller raises whichever input was last touched, so the handles never lock at the extremes — the Firefox bug the CSS version documented as unfixable becomes three lines against data-top and a data-[top=true]:z-20 utility.
The histogram is the UI upgrade. Shoppers judge a price filter by how much stock sits in range; 24 bars behind the track answer that, and they recolour from the same --lo/--hi the fill uses.
Make it yours
- Range:
min/maxon both inputs, then the tick row and theTOTALconstant in the controller. - Accent:
--color-trs01-accretints fill, handles, tooltips, histogram and CTA. - Minimum gap: the
GAPconstant (10 units); set it to 0 to let the handles meet.
Gotchas — read before shipping
pointer-events: allon the thumb pseudo-element is the only reason two stacked inputs work at all — it cannot be expressed as a utility, so it stays in the stylesheet.- Always label BOTH inputs (
aria-label="Minimum price"/"Maximum price"): the visible handles arearia-hiddenspans and announce nothing. - The tooltips sit above the track on negative offset. A parent with
overflow-hiddenclips them — the card usesoverflow-visibleand top padding instead.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
The @property-registered --lo/--hi and Tailwind v4's own sheet set the floor; the dual-thumb z-index swap makes the Firefox pointer-events limitation a non-issue.