<div id="rs" class="rs rs--pristine">
<div class="rs__range-wrap">
<input class="rs__range" type="range" data-range>
</div>
<div class="rs__rings">
<button class="rs__handle" type="button" data-handle>
<span class="rs__handle-value" data-value>0</span>
</button>
<div class="rs__ring"></div>
<div class="rs__ring"></div>
<div class="rs__ring"></div>
<div class="rs__ring"></div>
<div class="rs__ring"></div>
<div class="rs__ring"></div>
<div class="rs__ring"></div>
</div>
</div>
* {
border: 0;
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--hue: 223;
--bg: hsl(var(--hue),10%,90%);
--fg: hsl(var(--hue),10%,10%);
--primary: hsl(var(--hue),90%,55%);
--trans-dur: 0.3s;
font-size: calc(16px + (24 - 16) * (100vw - 320px) / (1280 - 320));
}
body,
button,
input {
font: 1em/1.5 "Source Code Pro", monospace;
}
body {
background-color: var(--bg);
color: var(--fg);
height: 100vh;
display: grid;
overflow: hidden;
place-items: center;
transition:
background-color var(--trans-dur),
color var(--trans-dur);
}
.rs,
.rs__rings {
position: relative;
}
.rs {
--rel-value: 0;
--range-units: 1;
transition: transform 0.3s ease-in-out;
}
.rs__range-wrap,
.rs__rings {
transform: rotateX(30deg) rotateY(30deg);
transform-style: preserve-3d;
}
.rs__range:focus,
.rs__handle:focus {
outline: transparent;
}
.rs__range-wrap {
display: none;
position: absolute;
inset: 0;
z-index: 1;
}
.rs__range {
background-color: transparent;
position: absolute;
top: 50%;
left: 50%;
width: 24em;
height: 12em;
transform: translate(-50%,-50%) rotateX(90deg) rotateZ(90deg);
-webkit-appearance: none;
appearance: none;
}
.rs__range::-webkit-slider-thumb {
background-color: transparent;
border: 0;
width: 12em;
height: 8em;
transform: skewY(30deg);
-webkit-appearance: none;
appearance: none;
}
.rs__range::-moz-range-thumb {
background-color: transparent;
border: 0;
width: 12em;
height: 8em;
transform: skewY(30deg);
}
.rs__rings {
z-index: 0;
}
.rs__handle,
.rs__ring {
border-radius: 2em;
box-shadow: 0 0 0 0.25em inset;
}
.rs__handle {
background-color: transparent;
color: var(--fg);
position: relative;
width: 8em;
height: 8em;
transition:
box-shadow var(--trans-dur),
color var(--trans-dur),
transform var(--trans-dur) linear;
-webkit-appearance: none;
appearance: none;
-webkit-tap-highlight-color: transparent;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
}
.rs__handle:before {
animation: handleBlur var(--trans-dur) ease-in-out;
border-radius: 50%;
box-shadow: 0 0 0 0 inset;
content: "";
display: block;
opacity: 0.1;
position: absolute;
top: -25%;
left: -25%;
width: 150%;
height: 150%;
}
.rs__handle:focus {
color: var(--primary);
}
.rs__handle-value {
font-size: 3em;
font-weight: 300;
line-height: 1;
}
.rs__ring {
position: absolute;
inset: 0;
opacity: 0.1;
pointer-events: none;
visibility: hidden;
transition:
transform var(--trans-dur) linear,
visibility var(--trans-dur) steps(1);
}
.rs--pristine .rs__handle:before {
animation: none;
}
.rs--active .rs__handle {
pointer-events: none;
transform: translateZ(calc(-6em + (var(--rel-value) / var(--range-units) * 12em)));
}
.rs--active .rs__handle:before {
animation: handleFocus 0.6s ease-in-out;
box-shadow: 0 0 0 6em inset;
transform: scale(0.4);
}
.rs--active .rs__range-wrap { display: block; }
.rs--active .rs__ring {
visibility: visible;
transition-duration: var(--trans-dur), 0s;
}
.rs--active .rs__ring:nth-of-type(1) { transform: translateZ(6em); }
.rs--active .rs__ring:nth-of-type(2) { transform: translateZ(4em); }
.rs--active .rs__ring:nth-of-type(3) { transform: translateZ(2em); }
.rs--active .rs__ring:nth-of-type(4) { transform: translateZ(0); }
.rs--active .rs__ring:nth-of-type(5) { transform: translateZ(-2em); }
.rs--active .rs__ring:nth-of-type(6) { transform: translateZ(-4em); }
.rs--active .rs__ring:nth-of-type(7) { transform: translateZ(-6em); }
/* Dark theme */
@media (prefers-color-scheme: dark) {
:root {
--bg: hsl(var(--hue),10%,10%);
--fg: hsl(var(--hue),10%,90%);
--primary: hsl(var(--hue),90%,65%);
}
}
/* Animations */
@keyframes handleBlur {
from {
box-shadow: 0 0 0 6em inset;
transform: scale(0.4);
}
to {
box-shadow: 0 0 0 0.1em inset;
transform: scale(1);
}
}
@keyframes handleFocus {
from { transform: scale(0); }
33% { transform: scale(0.5); }
67% { transform: scale(0.35); }
to { transform: scale(0.4); }
}
window.addEventListener("DOMContentLoaded",() => {
const slider = new _3DRangeSlider("#rs");
});
class _3DRangeSlider {
constructor(qs) {
this.el = document.querySelector(qs);
this.handle = this.el?.querySelector("[data-handle]");
this.range = this.el?.querySelector("[data-range]");
this.value = 3;
this.min = 0;
this.max = 6;
this.step = 1;
this.active = false;
this.rangeFocusTimeout = null;
this.init();
}
init() {
if (this.el) {
this.handle?.addEventListener("click",this.expand.bind(this));
if (this.range) {
this.range.value = this.value;
this.range.min = this.min;
this.range.max = this.max;
this.range.step = this.step;
this.range.addEventListener("input",this.adjust.bind(this));
document.addEventListener("click",this.collapse.bind(this));
document.addEventListener("keydown",this.collapse.bind(this));
}
this.updateDisplay();
}
}
expand() {
this.active = true;
this.handle.blur();
this.handle.disabled = this.active;
this.el.classList.remove("rs--pristine");
this.el.classList.add("rs--active");
clearTimeout(this.rangeFocusTimeout);
this.rangeFocusTimeout = setTimeout(() => this.range.focus(),1);
}
collapse(e) {
if (e.key === "Escape" || (!e.key && e.target === document.body)) {
this.active = false;
this.handle.disabled = this.active;
this.el.classList.remove("rs--active");
}
}
adjust(e) {
this.value = e.target.value;
this.updateDisplay();
}
updateDisplay() {
if (this.el) {
// CSS variables
this.el.style.setProperty("--rel-value",this.value - this.min);
this.el.style.setProperty("--range-units",this.max - this.min);
// displayed value
const value = this.el.querySelector("[data-value]");
if (value)
value.textContent = this.value;
}
}
}
This Pen doesn't use any external JavaScript resources.