17 CSS Range Sliders09 / 17
Gradient Fill Progress Slider
A clean course-progress slider where the filled portion carries a continuous sunset-orange-to-violet gradient, the percentage display mirrors the same gradient via background-clip, and milestone cards illuminate as thresholds are crossed.
Published
The code
<div class="rs-09">
<div class="rs-09__card">
<div class="rs-09__top">
<div>
<div class="rs-09__title">Course Progress</div>
<div class="rs-09__sub">Drag to explore your journey</div>
</div>
<div class="rs-09__pct-badge"><span id="rs-09-pct">67</span>%</div>
</div>
<div class="rs-09__track-wrap">
<div class="rs-09__track-bg"></div>
<div class="rs-09__track-fill" id="rs-09-fill"></div>
<input class="rs-09__input" type="range" min="0" max="100" value="67" id="rs-09-inp">
</div>
<div class="rs-09__steps" id="rs-09-steps">
<div class="rs-09__step active" data-min="0">
<div class="rs-09__step-icon">🏁</div>
<div class="rs-09__step-name">Foundations</div>
<div class="rs-09__step-desc">0–33% complete</div>
</div>
<div class="rs-09__step active" data-min="34">
<div class="rs-09__step-icon">🔬</div>
<div class="rs-09__step-name">Deep Dive</div>
<div class="rs-09__step-desc">34–66% complete</div>
</div>
<div class="rs-09__step" data-min="67">
<div class="rs-09__step-icon">🚀</div>
<div class="rs-09__step-name">Mastery</div>
<div class="rs-09__step-desc">67–100% complete</div>
</div>
</div>
</div>
</div><div class="rs-09">
<div class="rs-09__card">
<div class="rs-09__top">
<div>
<div class="rs-09__title">Course Progress</div>
<div class="rs-09__sub">Drag to explore your journey</div>
</div>
<div class="rs-09__pct-badge"><span id="rs-09-pct">67</span>%</div>
</div>
<div class="rs-09__track-wrap">
<div class="rs-09__track-bg"></div>
<div class="rs-09__track-fill" id="rs-09-fill"></div>
<input class="rs-09__input" type="range" min="0" max="100" value="67" id="rs-09-inp">
</div>
<div class="rs-09__steps" id="rs-09-steps">
<div class="rs-09__step active" data-min="0">
<div class="rs-09__step-icon">🏁</div>
<div class="rs-09__step-name">Foundations</div>
<div class="rs-09__step-desc">0–33% complete</div>
</div>
<div class="rs-09__step active" data-min="34">
<div class="rs-09__step-icon">🔬</div>
<div class="rs-09__step-name">Deep Dive</div>
<div class="rs-09__step-desc">34–66% complete</div>
</div>
<div class="rs-09__step" data-min="67">
<div class="rs-09__step-icon">🚀</div>
<div class="rs-09__step-name">Mastery</div>
<div class="rs-09__step-desc">67–100% complete</div>
</div>
</div>
</div>
</div>.rs-09,
.rs-09 *,
.rs-09 *::before,
.rs-09 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.rs-09 ::selection {
background: #f472b6;
color: #fff;
}
.rs-09 {
--bg: #fafafa;
--card: #ffffff;
--grad: linear-gradient(90deg,#f97316,#ec4899,#a855f7);
--text: #111827;
--muted: #9ca3af;
--border: #f3f4f6;
background: var(--bg);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 40px 24px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.rs-09__card {
background: var(--card);
border-radius: 24px;
padding: 36px;
max-width: 440px;
width: 100%;
box-shadow: 0 4px 24px rgba(0,0,0,0.06), 0 1px 4px rgba(0,0,0,0.04);
}
.rs-09__top {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 8px;
}
.rs-09__title {
font-size: 20px;
font-weight: 800;
color: var(--text);
}
.rs-09__pct-badge {
font-size: 28px;
font-weight: 900;
letter-spacing: -1px;
background: var(--grad);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-variant-numeric: tabular-nums;
}
.rs-09__sub {
font-size: 13px;
color: var(--muted);
margin-bottom: 36px;
}
.rs-09__track-wrap {
position: relative;
height: 48px;
display: flex;
align-items: center;
margin-bottom: 32px;
padding: 0 12px;
}
.rs-09__track-bg {
position: absolute;
left: 12px;
right: 12px;
top: 50%;
transform: translateY(-50%);
height: 12px;
background: var(--border);
border-radius: 99px;
}
.rs-09__track-fill {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
height: 12px;
background: var(--grad);
border-radius: 99px;
transition: width 0.05s;
}
.rs-09__input {
position: absolute;
width: calc(100% - 24px);
left: 12px;
top: 0;
-webkit-appearance: none;
appearance: none;
height: 48px;
background: transparent;
cursor: pointer;
outline: none;
z-index: 2;
margin: 0;
}
.rs-09__input::-webkit-slider-runnable-track {
height: 12px;
background: transparent;
border-radius: 99px;
}
.rs-09__input::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 24px;
height: 24px;
border-radius: 50%;
background: #fff;
border: 4px solid #ec4899;
box-shadow: 0 0 0 6px rgba(236,72,153,0.12), 0 4px 16px rgba(0,0,0,0.15);
cursor: grab;
transition: transform 0.15s, box-shadow 0.15s;
margin-top: -6px;
}
.rs-09__input:active::-webkit-slider-thumb {
transform: scale(1.15);
box-shadow: 0 0 0 10px rgba(236,72,153,0.15), 0 4px 16px rgba(0,0,0,0.15);
cursor: grabbing;
}
.rs-09__input::-moz-range-track {
height: 12px;
background: transparent;
border-radius: 99px;
}
.rs-09__input::-moz-range-thumb {
width: 24px;
height: 24px;
border-radius: 50%;
border: 4px solid #ec4899;
background: #fff;
box-shadow: 0 0 0 6px rgba(236,72,153,0.12), 0 4px 16px rgba(0,0,0,0.15);
cursor: grab;
box-sizing: border-box;
}
.rs-09__steps {
display: grid;
grid-template-columns: repeat(3,1fr);
gap: 12px;
}
.rs-09__step {
padding: 14px;
border-radius: 14px;
background: var(--border);
transition: background 0.3s, box-shadow 0.3s;
}
.rs-09__step.active {
background: linear-gradient(135deg, rgba(249,115,22,0.1), rgba(168,85,247,0.1));
box-shadow: inset 0 0 0 1px rgba(236,72,153,0.25);
}
.rs-09__step-icon {
font-size: 22px;
margin-bottom: 6px;
}
.rs-09__step-name {
font-size: 12px;
font-weight: 700;
color: var(--text);
margin-bottom: 2px;
}
.rs-09__step-desc {
font-size: 10px;
color: var(--muted);
}
@media (prefers-reduced-motion: reduce) {
.rs-09__track-fill,
.rs-09__step {
transition: none;
}
}.rs-09,
.rs-09 *,
.rs-09 *::before,
.rs-09 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.rs-09 ::selection {
background: #f472b6;
color: #fff;
}
.rs-09 {
--bg: #fafafa;
--card: #ffffff;
--grad: linear-gradient(90deg,#f97316,#ec4899,#a855f7);
--text: #111827;
--muted: #9ca3af;
--border: #f3f4f6;
background: var(--bg);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 40px 24px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.rs-09__card {
background: var(--card);
border-radius: 24px;
padding: 36px;
max-width: 440px;
width: 100%;
box-shadow: 0 4px 24px rgba(0,0,0,0.06), 0 1px 4px rgba(0,0,0,0.04);
}
.rs-09__top {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 8px;
}
.rs-09__title {
font-size: 20px;
font-weight: 800;
color: var(--text);
}
.rs-09__pct-badge {
font-size: 28px;
font-weight: 900;
letter-spacing: -1px;
background: var(--grad);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-variant-numeric: tabular-nums;
}
.rs-09__sub {
font-size: 13px;
color: var(--muted);
margin-bottom: 36px;
}
.rs-09__track-wrap {
position: relative;
height: 48px;
display: flex;
align-items: center;
margin-bottom: 32px;
padding: 0 12px;
}
.rs-09__track-bg {
position: absolute;
left: 12px;
right: 12px;
top: 50%;
transform: translateY(-50%);
height: 12px;
background: var(--border);
border-radius: 99px;
}
.rs-09__track-fill {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
height: 12px;
background: var(--grad);
border-radius: 99px;
transition: width 0.05s;
}
.rs-09__input {
position: absolute;
width: calc(100% - 24px);
left: 12px;
top: 0;
-webkit-appearance: none;
appearance: none;
height: 48px;
background: transparent;
cursor: pointer;
outline: none;
z-index: 2;
margin: 0;
}
.rs-09__input::-webkit-slider-runnable-track {
height: 12px;
background: transparent;
border-radius: 99px;
}
.rs-09__input::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 24px;
height: 24px;
border-radius: 50%;
background: #fff;
border: 4px solid #ec4899;
box-shadow: 0 0 0 6px rgba(236,72,153,0.12), 0 4px 16px rgba(0,0,0,0.15);
cursor: grab;
transition: transform 0.15s, box-shadow 0.15s;
margin-top: -6px;
}
.rs-09__input:active::-webkit-slider-thumb {
transform: scale(1.15);
box-shadow: 0 0 0 10px rgba(236,72,153,0.15), 0 4px 16px rgba(0,0,0,0.15);
cursor: grabbing;
}
.rs-09__input::-moz-range-track {
height: 12px;
background: transparent;
border-radius: 99px;
}
.rs-09__input::-moz-range-thumb {
width: 24px;
height: 24px;
border-radius: 50%;
border: 4px solid #ec4899;
background: #fff;
box-shadow: 0 0 0 6px rgba(236,72,153,0.12), 0 4px 16px rgba(0,0,0,0.15);
cursor: grab;
box-sizing: border-box;
}
.rs-09__steps {
display: grid;
grid-template-columns: repeat(3,1fr);
gap: 12px;
}
.rs-09__step {
padding: 14px;
border-radius: 14px;
background: var(--border);
transition: background 0.3s, box-shadow 0.3s;
}
.rs-09__step.active {
background: linear-gradient(135deg, rgba(249,115,22,0.1), rgba(168,85,247,0.1));
box-shadow: inset 0 0 0 1px rgba(236,72,153,0.25);
}
.rs-09__step-icon {
font-size: 22px;
margin-bottom: 6px;
}
.rs-09__step-name {
font-size: 12px;
font-weight: 700;
color: var(--text);
margin-bottom: 2px;
}
.rs-09__step-desc {
font-size: 10px;
color: var(--muted);
}
@media (prefers-reduced-motion: reduce) {
.rs-09__track-fill,
.rs-09__step {
transition: none;
}
}(function(){
const inp = document.getElementById('rs-09-inp');
const fill = document.getElementById('rs-09-fill');
const pct = document.getElementById('rs-09-pct');
const steps = document.querySelectorAll('.rs-09__step');
const thresholds = [0, 34, 67];
// Track recess matches the input's left:12px + width:calc(100% - 24px)
// so the thumb-center travels from 12px to (wrap-width - 12px). The
// fill must use the same recessed coordinate space — % of the
// input's effective range, NOT % of the full wrap — otherwise the
// gradient extends past the thumb (because % of full-wrap > % of
// recessed-range at high values).
const trackInset = 12;
function update(){
const v = +inp.value;
const min = +inp.min || 0;
const max = +inp.max || 100;
const wrapW = inp.parentElement.getBoundingClientRect().width;
const usable = Math.max(0, wrapW - trackInset * 2);
const frac = (v - min) / (max - min);
fill.style.width = (usable * frac) + 'px';
pct.textContent = v;
steps.forEach((s, i) => {
s.classList.toggle('active', v >= thresholds[i]);
});
}
inp.addEventListener('input', update);
window.addEventListener('resize', update);
update();
})();(function(){
const inp = document.getElementById('rs-09-inp');
const fill = document.getElementById('rs-09-fill');
const pct = document.getElementById('rs-09-pct');
const steps = document.querySelectorAll('.rs-09__step');
const thresholds = [0, 34, 67];
// Track recess matches the input's left:12px + width:calc(100% - 24px)
// so the thumb-center travels from 12px to (wrap-width - 12px). The
// fill must use the same recessed coordinate space — % of the
// input's effective range, NOT % of the full wrap — otherwise the
// gradient extends past the thumb (because % of full-wrap > % of
// recessed-range at high values).
const trackInset = 12;
function update(){
const v = +inp.value;
const min = +inp.min || 0;
const max = +inp.max || 100;
const wrapW = inp.parentElement.getBoundingClientRect().width;
const usable = Math.max(0, wrapW - trackInset * 2);
const frac = (v - min) / (max - min);
fill.style.width = (usable * frac) + 'px';
pct.textContent = v;
steps.forEach((s, i) => {
s.classList.toggle('active', v >= thresholds[i]);
});
}
inp.addEventListener('input', update);
window.addEventListener('resize', update);
update();
})();Here's a working CSS Range Slider 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: Gradient Fill Progress Slider
Source: https://codefronts.com/components/css-range-sliders/gradient-fill-progress-slider/
A clean course-progress slider where the filled portion carries a continuous sunset-orange-to-violet gradient, the percentage display mirrors the same gradient via background-clip, and milestone cards illuminate as thresholds are crossed.
## HTML
```html
<div class="rs-09">
<div class="rs-09__card">
<div class="rs-09__top">
<div>
<div class="rs-09__title">Course Progress</div>
<div class="rs-09__sub">Drag to explore your journey</div>
</div>
<div class="rs-09__pct-badge"><span id="rs-09-pct">67</span>%</div>
</div>
<div class="rs-09__track-wrap">
<div class="rs-09__track-bg"></div>
<div class="rs-09__track-fill" id="rs-09-fill"></div>
<input class="rs-09__input" type="range" min="0" max="100" value="67" id="rs-09-inp">
</div>
<div class="rs-09__steps" id="rs-09-steps">
<div class="rs-09__step active" data-min="0">
<div class="rs-09__step-icon">🏁</div>
<div class="rs-09__step-name">Foundations</div>
<div class="rs-09__step-desc">0–33% complete</div>
</div>
<div class="rs-09__step active" data-min="34">
<div class="rs-09__step-icon">🔬</div>
<div class="rs-09__step-name">Deep Dive</div>
<div class="rs-09__step-desc">34–66% complete</div>
</div>
<div class="rs-09__step" data-min="67">
<div class="rs-09__step-icon">🚀</div>
<div class="rs-09__step-name">Mastery</div>
<div class="rs-09__step-desc">67–100% complete</div>
</div>
</div>
</div>
</div>
```
## CSS
```css
.rs-09,
.rs-09 *,
.rs-09 *::before,
.rs-09 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.rs-09 ::selection {
background: #f472b6;
color: #fff;
}
.rs-09 {
--bg: #fafafa;
--card: #ffffff;
--grad: linear-gradient(90deg,#f97316,#ec4899,#a855f7);
--text: #111827;
--muted: #9ca3af;
--border: #f3f4f6;
background: var(--bg);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 40px 24px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.rs-09__card {
background: var(--card);
border-radius: 24px;
padding: 36px;
max-width: 440px;
width: 100%;
box-shadow: 0 4px 24px rgba(0,0,0,0.06), 0 1px 4px rgba(0,0,0,0.04);
}
.rs-09__top {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 8px;
}
.rs-09__title {
font-size: 20px;
font-weight: 800;
color: var(--text);
}
.rs-09__pct-badge {
font-size: 28px;
font-weight: 900;
letter-spacing: -1px;
background: var(--grad);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-variant-numeric: tabular-nums;
}
.rs-09__sub {
font-size: 13px;
color: var(--muted);
margin-bottom: 36px;
}
.rs-09__track-wrap {
position: relative;
height: 48px;
display: flex;
align-items: center;
margin-bottom: 32px;
padding: 0 12px;
}
.rs-09__track-bg {
position: absolute;
left: 12px;
right: 12px;
top: 50%;
transform: translateY(-50%);
height: 12px;
background: var(--border);
border-radius: 99px;
}
.rs-09__track-fill {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
height: 12px;
background: var(--grad);
border-radius: 99px;
transition: width 0.05s;
}
.rs-09__input {
position: absolute;
width: calc(100% - 24px);
left: 12px;
top: 0;
-webkit-appearance: none;
appearance: none;
height: 48px;
background: transparent;
cursor: pointer;
outline: none;
z-index: 2;
margin: 0;
}
.rs-09__input::-webkit-slider-runnable-track {
height: 12px;
background: transparent;
border-radius: 99px;
}
.rs-09__input::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 24px;
height: 24px;
border-radius: 50%;
background: #fff;
border: 4px solid #ec4899;
box-shadow: 0 0 0 6px rgba(236,72,153,0.12), 0 4px 16px rgba(0,0,0,0.15);
cursor: grab;
transition: transform 0.15s, box-shadow 0.15s;
margin-top: -6px;
}
.rs-09__input:active::-webkit-slider-thumb {
transform: scale(1.15);
box-shadow: 0 0 0 10px rgba(236,72,153,0.15), 0 4px 16px rgba(0,0,0,0.15);
cursor: grabbing;
}
.rs-09__input::-moz-range-track {
height: 12px;
background: transparent;
border-radius: 99px;
}
.rs-09__input::-moz-range-thumb {
width: 24px;
height: 24px;
border-radius: 50%;
border: 4px solid #ec4899;
background: #fff;
box-shadow: 0 0 0 6px rgba(236,72,153,0.12), 0 4px 16px rgba(0,0,0,0.15);
cursor: grab;
box-sizing: border-box;
}
.rs-09__steps {
display: grid;
grid-template-columns: repeat(3,1fr);
gap: 12px;
}
.rs-09__step {
padding: 14px;
border-radius: 14px;
background: var(--border);
transition: background 0.3s, box-shadow 0.3s;
}
.rs-09__step.active {
background: linear-gradient(135deg, rgba(249,115,22,0.1), rgba(168,85,247,0.1));
box-shadow: inset 0 0 0 1px rgba(236,72,153,0.25);
}
.rs-09__step-icon {
font-size: 22px;
margin-bottom: 6px;
}
.rs-09__step-name {
font-size: 12px;
font-weight: 700;
color: var(--text);
margin-bottom: 2px;
}
.rs-09__step-desc {
font-size: 10px;
color: var(--muted);
}
@media (prefers-reduced-motion: reduce) {
.rs-09__track-fill,
.rs-09__step {
transition: none;
}
}
```
## JavaScript
```js
(function(){
const inp = document.getElementById('rs-09-inp');
const fill = document.getElementById('rs-09-fill');
const pct = document.getElementById('rs-09-pct');
const steps = document.querySelectorAll('.rs-09__step');
const thresholds = [0, 34, 67];
// Track recess matches the input's left:12px + width:calc(100% - 24px)
// so the thumb-center travels from 12px to (wrap-width - 12px). The
// fill must use the same recessed coordinate space — % of the
// input's effective range, NOT % of the full wrap — otherwise the
// gradient extends past the thumb (because % of full-wrap > % of
// recessed-range at high values).
const trackInset = 12;
function update(){
const v = +inp.value;
const min = +inp.min || 0;
const max = +inp.max || 100;
const wrapW = inp.parentElement.getBoundingClientRect().width;
const usable = Math.max(0, wrapW - trackInset * 2);
const frac = (v - min) / (max - min);
fill.style.width = (usable * frac) + 'px';
pct.textContent = v;
steps.forEach((s, i) => {
s.classList.toggle('active', v >= thresholds[i]);
});
}
inp.addEventListener('input', update);
window.addEventListener('resize', update);
update();
})();
```Here's a working CSS Range Slider 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: Gradient Fill Progress Slider
Source: https://codefronts.com/components/css-range-sliders/gradient-fill-progress-slider/
A clean course-progress slider where the filled portion carries a continuous sunset-orange-to-violet gradient, the percentage display mirrors the same gradient via background-clip, and milestone cards illuminate as thresholds are crossed.
## HTML
```html
<div class="rs-09">
<div class="rs-09__card">
<div class="rs-09__top">
<div>
<div class="rs-09__title">Course Progress</div>
<div class="rs-09__sub">Drag to explore your journey</div>
</div>
<div class="rs-09__pct-badge"><span id="rs-09-pct">67</span>%</div>
</div>
<div class="rs-09__track-wrap">
<div class="rs-09__track-bg"></div>
<div class="rs-09__track-fill" id="rs-09-fill"></div>
<input class="rs-09__input" type="range" min="0" max="100" value="67" id="rs-09-inp">
</div>
<div class="rs-09__steps" id="rs-09-steps">
<div class="rs-09__step active" data-min="0">
<div class="rs-09__step-icon">🏁</div>
<div class="rs-09__step-name">Foundations</div>
<div class="rs-09__step-desc">0–33% complete</div>
</div>
<div class="rs-09__step active" data-min="34">
<div class="rs-09__step-icon">🔬</div>
<div class="rs-09__step-name">Deep Dive</div>
<div class="rs-09__step-desc">34–66% complete</div>
</div>
<div class="rs-09__step" data-min="67">
<div class="rs-09__step-icon">🚀</div>
<div class="rs-09__step-name">Mastery</div>
<div class="rs-09__step-desc">67–100% complete</div>
</div>
</div>
</div>
</div>
```
## CSS
```css
.rs-09,
.rs-09 *,
.rs-09 *::before,
.rs-09 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.rs-09 ::selection {
background: #f472b6;
color: #fff;
}
.rs-09 {
--bg: #fafafa;
--card: #ffffff;
--grad: linear-gradient(90deg,#f97316,#ec4899,#a855f7);
--text: #111827;
--muted: #9ca3af;
--border: #f3f4f6;
background: var(--bg);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 40px 24px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.rs-09__card {
background: var(--card);
border-radius: 24px;
padding: 36px;
max-width: 440px;
width: 100%;
box-shadow: 0 4px 24px rgba(0,0,0,0.06), 0 1px 4px rgba(0,0,0,0.04);
}
.rs-09__top {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 8px;
}
.rs-09__title {
font-size: 20px;
font-weight: 800;
color: var(--text);
}
.rs-09__pct-badge {
font-size: 28px;
font-weight: 900;
letter-spacing: -1px;
background: var(--grad);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-variant-numeric: tabular-nums;
}
.rs-09__sub {
font-size: 13px;
color: var(--muted);
margin-bottom: 36px;
}
.rs-09__track-wrap {
position: relative;
height: 48px;
display: flex;
align-items: center;
margin-bottom: 32px;
padding: 0 12px;
}
.rs-09__track-bg {
position: absolute;
left: 12px;
right: 12px;
top: 50%;
transform: translateY(-50%);
height: 12px;
background: var(--border);
border-radius: 99px;
}
.rs-09__track-fill {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
height: 12px;
background: var(--grad);
border-radius: 99px;
transition: width 0.05s;
}
.rs-09__input {
position: absolute;
width: calc(100% - 24px);
left: 12px;
top: 0;
-webkit-appearance: none;
appearance: none;
height: 48px;
background: transparent;
cursor: pointer;
outline: none;
z-index: 2;
margin: 0;
}
.rs-09__input::-webkit-slider-runnable-track {
height: 12px;
background: transparent;
border-radius: 99px;
}
.rs-09__input::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 24px;
height: 24px;
border-radius: 50%;
background: #fff;
border: 4px solid #ec4899;
box-shadow: 0 0 0 6px rgba(236,72,153,0.12), 0 4px 16px rgba(0,0,0,0.15);
cursor: grab;
transition: transform 0.15s, box-shadow 0.15s;
margin-top: -6px;
}
.rs-09__input:active::-webkit-slider-thumb {
transform: scale(1.15);
box-shadow: 0 0 0 10px rgba(236,72,153,0.15), 0 4px 16px rgba(0,0,0,0.15);
cursor: grabbing;
}
.rs-09__input::-moz-range-track {
height: 12px;
background: transparent;
border-radius: 99px;
}
.rs-09__input::-moz-range-thumb {
width: 24px;
height: 24px;
border-radius: 50%;
border: 4px solid #ec4899;
background: #fff;
box-shadow: 0 0 0 6px rgba(236,72,153,0.12), 0 4px 16px rgba(0,0,0,0.15);
cursor: grab;
box-sizing: border-box;
}
.rs-09__steps {
display: grid;
grid-template-columns: repeat(3,1fr);
gap: 12px;
}
.rs-09__step {
padding: 14px;
border-radius: 14px;
background: var(--border);
transition: background 0.3s, box-shadow 0.3s;
}
.rs-09__step.active {
background: linear-gradient(135deg, rgba(249,115,22,0.1), rgba(168,85,247,0.1));
box-shadow: inset 0 0 0 1px rgba(236,72,153,0.25);
}
.rs-09__step-icon {
font-size: 22px;
margin-bottom: 6px;
}
.rs-09__step-name {
font-size: 12px;
font-weight: 700;
color: var(--text);
margin-bottom: 2px;
}
.rs-09__step-desc {
font-size: 10px;
color: var(--muted);
}
@media (prefers-reduced-motion: reduce) {
.rs-09__track-fill,
.rs-09__step {
transition: none;
}
}
```
## JavaScript
```js
(function(){
const inp = document.getElementById('rs-09-inp');
const fill = document.getElementById('rs-09-fill');
const pct = document.getElementById('rs-09-pct');
const steps = document.querySelectorAll('.rs-09__step');
const thresholds = [0, 34, 67];
// Track recess matches the input's left:12px + width:calc(100% - 24px)
// so the thumb-center travels from 12px to (wrap-width - 12px). The
// fill must use the same recessed coordinate space — % of the
// input's effective range, NOT % of the full wrap — otherwise the
// gradient extends past the thumb (because % of full-wrap > % of
// recessed-range at high values).
const trackInset = 12;
function update(){
const v = +inp.value;
const min = +inp.min || 0;
const max = +inp.max || 100;
const wrapW = inp.parentElement.getBoundingClientRect().width;
const usable = Math.max(0, wrapW - trackInset * 2);
const frac = (v - min) / (max - min);
fill.style.width = (usable * frac) + 'px';
pct.textContent = v;
steps.forEach((s, i) => {
s.classList.toggle('active', v >= thresholds[i]);
});
}
inp.addEventListener('input', update);
window.addEventListener('resize', update);
update();
})();
```How this works
The gradient fill is a div.rs-09__track-fill with a fixed linear-gradient(90deg, #f97316, #ec4899, #a855f7) background. Its width is set as a percentage in JS — the gradient always spans the full width so the visible color at the thumb always reflects the current position on the spectrum. The large percentage number uses the same gradient via -webkit-background-clip: text; -webkit-text-fill-color: transparent, making the number and track visually unified.
The ring thumb is achieved by setting background: transparent and a thick border with a gradient via border-image. However, since border-radius and border-image are incompatible in CSS, the ring effect uses background-clip: content-box with transparent fill instead — a technique that works on WebKit. The three milestone cards use data-min thresholds checked against the current value to toggle the .active illuminated state.
Make it yours
- Change the gradient palette by editing the
--grad: linear-gradient(90deg,...)variable — update it in both the track fill background and the percentage badge to keep them in sync. - Increase the number of milestone cards by adding more
.rs-09__stepdivs withdata-minvalues and updating thethresholds[]array in JS. - Add a striped texture to the unfilled track by layering a
repeating-linear-gradientat 45° on top of the plain background in.rs-09__track-bg. - Animate the milestone cards on activation by adding a
@keyframes rs-09-popthat scales from0.95to1.05back to1, triggered by adding and removing a class via JS. - Connect to local storage to persist progress across page reloads: on
loadreadlocalStorage.getItem('rs09-progress')and setinp.valueaccordingly, saving on each input event.
Gotchas — read before shipping
-webkit-text-fill-color: transparentcombined withbackground-clip: textis a non-standard property — always pair it with a fallback plaincolorrule for environments where the gradient text fails.- The border-image + border-radius incompatibility is a long-standing CSS limitation — the transparent ring thumb workaround works on WebKit but may need adjustment on Firefox where the thumb is rendered differently.
- Setting
widthon the fill div via JS on every input event is performant, but adding a CSStransition: widthcan cause lag on rapid dragging — keep transition duration to0.05sor omit it entirely for live-drag scenarios.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 80+ | 14+ | 78+ | 80+ |
Gradient text via background-clip: text is supported in all modern browsers; the transparent ring thumb requires per-browser thumb pseudo-element rules.