<a class="design-reference" href="//100daysui.com/#cbp=ajax/shot-90" target="_blank">100 Days UI Challenge - Day 90</a>
<div class="component">
<aside>
<div class="preamp">
<label>preamp</label>
</div>
</aside>
<main>
<div class="presets">
<label>Presets:</label>
<select name="Custom" onchange="app.selectPreset(this)">
<option value="custom">Custom</option>
<option value="rock">Rock</option>
<option value="pop">Pop</option>
<option value="pop">Classical</option>
<option value="pop">Disco</option>
</select>
<button class="reset" onclick="app.reset()">Reset</button>
</div>
<div class="sliders">
<svg preserveAspectRatio="none" viewBox="0 0 140 100">
<path d="" class="line-shadow"></path>
<path d="" class="line"></path>
</svg>
<div class="range-slider">
<input type="range" orient="vertical" min="0" max="100" />
<div class="range-slider__bar"></div>
<div class="range-slider__thumb"></div>
</div>
<div class="range-slider">
<input type="range" orient="vertical" min="0" max="100" />
<div class="range-slider__bar"></div>
<div class="range-slider__thumb"></div>
</div>
<div class="range-slider">
<input type="range" orient="vertical" min="0" max="100" />
<div class="range-slider__bar"></div>
<div class="range-slider__thumb"></div>
</div>
<div class="range-slider">
<input type="range" orient="vertical" min="0" max="100" />
<div class="range-slider__bar"></div>
<div class="range-slider__thumb"></div>
</div>
<div class="range-slider">
<input type="range" orient="vertical" min="0" max="100" />
<div class="range-slider__bar"></div>
<div class="range-slider__thumb"></div>
</div>
<div class="range-slider">
<input type="range" orient="vertical" min="0" max="100" />
<div class="range-slider__bar"></div>
<div class="range-slider__thumb"></div>
</div>
<div class="range-slider">
<input type="range" orient="vertical" min="0" max="100" />
<div class="range-slider__bar"></div>
<div class="range-slider__thumb"></div>
</div>
</div>
</main>
</div>
// If those values are updated, remember to update in JS
$slider-thumb-size: 20px;
$slider-height: 300px;
$slider-track-thickness: 4px;
$slider-width: 80px;
$slider-width-device-small: 40px;
$color-theme: #3D3D4A;
$color-track: #343440;
$color-text: lighten(#737383,10%);
$border-radius: 10px;
@mixin device-bigger {
@media (min-width: 800px) { @content; }
}
// The input range mixin code is based on Ana Tudor's pen codepen.io/thebabydino/pen/pvLPOQ
@mixin track {
border: none;
background: $color-track;
width: $slider-track-thickness;
border-color: $color-track;
border-radius: 10px;
box-shadow: 0 0 0 2px $color-theme;
}
@mixin thumb {
position: relative;
// Increase hitbox
width: $slider-thumb-size*2;
height: $slider-thumb-size*2;
opacity: 0; // Hide the native styling
}
*{
outline: none;
}
*, *:before,*:after{
box-sizing: border-box;
}
html,
body{
height: 100%;
}
body{
margin: 0;
padding: 10px;
min-height: 400px;
font-family: sans-serif;
display: flex;
justify-content: center;
align-items: flex-start;
@media(min-height: 500px){
align-items: center;
}
font: 16px sans-serif;
background: radial-gradient(#B3B1CB,#E1DEFE);
}
.component{
position: relative;
color: white;
background-color: $color-theme;
border-radius: $border-radius;
box-shadow: 0px 20px 40px rgba(black,.5),0px -2px 40px rgba(black,.3);
//box-shadow:0 1px 4px rgba(0, 0, 0, 0.3), 0 0 40px rgba(0, 0, 0, 0.1) inset;
min-width: 280px;
display: flex;
flex-direction: column;
@include device-bigger {
flex-direction: row;
}
&:before,&:after{
content:'';
background-color: transparent;
position:absolute;
z-index: -1;
box-shadow: 0 20px 20px rgba(black,.3);
top:100%;
bottom:-5px;
left:8%;
right:8%;
border-radius: 50%;
}
&:after{
box-shadow: 0 25px 20px rgba(black,.6);
left:12%;
right:12%;
}
aside{
// outline: 1px dashed deeppink; // debug
position: relative;
display: block;
background: #373641;
border-top-left-radius: $border-radius;
border-top-right-radius: $border-radius;
@include device-bigger {
border-top-right-radius: 0;
border-bottom-left-radius: $border-radius;
min-width: 140px;
}
.preamp{
height: 70px;
@include device-bigger {
height: 100px;
}
display: flex;
justify-content: center;
align-items: center;
> label{
color: #DEDFE4;
text-transform: uppercase;
display: block;
font-weight: 700;
}
}
}
main{
position: relative;
display: block;
padding-bottom: 50px;
@include device-bigger {
padding-left: 20px;
padding-right: 20px;
}
.presets{
display: flex;
align-items: center;
justify-content: center;
padding: 10px;
padding-left: calc(#{$slider-width-device-small}/2 - .5em);
height: 100px;
@include device-bigger {
padding-left: calc(#{$slider-width}/2 - .5em);
justify-content: flex-start;
}
color: $color-text;
font-weight: 700;
border-color: #4F4F62;
background: transparent;
> label{
display: inline-block;
margin-right: 20px;
}
> select{
appearance: none;
border-radius: 8px;
border: 2px solid currentColor;
max-width: 200px;
padding: 4px;
@include device-bigger {
min-width: 200px;
padding-left: 10px;
}
color: inherit;
background: transparent;
border-color: inherit;
height: 30px;
margin-right: 10px;
option{
background-color: $color-theme;
}
}
> button{
height: 30px;
@include device-bigger {
min-width: 80px;
}
border-radius: 8px;
background: transparent;
color: inherit;
border: 2px solid currentColor;
border-color: inherit;
padding: 4px 10px;
cursor: pointer;
outline: none;
}
}
.sliders{
position: relative;
display: inline-block;
.range-slider {
display: inline-block;
width: $slider-width-device-small;
@include device-bigger {
width: $slider-width;
}
position: relative;
height: $slider-height;
float: left;
&::after{
position: absolute;
bottom: -24px;
left: calc(50% - 2em);
font-size: 80%;
color: $color-text;
content: '32';
width: 4em;
text-align: center;
}
&:nth-child(2)::after{
content: '32';
}
&:nth-child(3)::after{
content: '64';
}
&:nth-child(4)::after{
content: '128';
}
&:nth-child(5)::after{
content: '256';
}
&:nth-child(6)::after{
content: '512';
}
&:nth-child(7)::after{
content: '1K';
}
&:nth-child(8)::after{
content: '2K';
}
&__thumb{
opacity: 1;
position: absolute;
left: $slider-width-device-small/2 - $slider-thumb-size/2;
@include device-bigger {
left: $slider-width/2 - $slider-thumb-size/2;
}
width: $slider-thumb-size;
height: $slider-thumb-size;
line-height: $slider-thumb-size;
background-color: white;
color: #8376FF;
text-align: center;
font-size: 40%;
box-shadow: 0 0 2px #373641;
border-radius: 50%;
pointer-events: none;
cursor: pointer;
z-index: 2;
}
&__bar{
left: $slider-width-device-small/2 - $slider-track-thickness/2;
@include device-bigger {
left: $slider-width/2 - $slider-track-thickness/2;
}
bottom: 0;
position: absolute;
background: linear-gradient(#9791B8,#8376FF);
pointer-events: none;
width: $slider-track-thickness;
border-radius: 10px;
opacity: 1;
}
input[type=range][orient=vertical]
{
//outline: 1px dashed white; // debug
position: relative;
margin: 0;
height: 100%;
width: 100%;
display: inline-block;
position: relative;
writing-mode: bt-lr; // IE
appearance: slider-vertical; // webkit
&::slider-runnable-track,
&::slider-thumb {
appearance: none;
}
&::slider-runnable-track {
@include track;
}
&::range-track {
@include track;
}
&::track {
@include track;
color: transparent;
height: 100%;
}
&::fill-lower,
&::fill-upper,
&::tooltip {
display: none;
}
&::slider-thumb {
left: -$slider-thumb-size; // fix Ipad hitbox
@include thumb;
}
&::range-thumb {
@include thumb;
}
&::thumb {
@include thumb;
}
}
}
svg{
z-index: 1;
overflow: visible;
pointer-events: none;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
fill: none;
stroke-width: 1;
.line{
stroke: #F7ED7D;
}
.line-shadow{
z-index: 1;
stroke-width: 2;
stroke: #252525;
opacity: .35;
display: none;
@include device-bigger{
display: block;
}
}
}
}
}
}
.design-reference{
position: fixed;
bottom: 6px;
right: 6px;
color: $color-text;
font-size: 70%;
display: none;
@media(min-height: 600px){
display: block;
}
}
View Compiled
// String formatter
if (!String.prototype.format) {
String.prototype.format = function() {
var args = arguments;
return this.replace(/{(\d+)}/g, function(match, number) {
return typeof args[number] != 'undefined' ? args[number] : match;
});
};
}
let app = (() => {
const $svgLine = document.querySelector('svg .line');
const $svgLineShadow = document.querySelector('svg .line-shadow');
const sliderThumbSize = 20;
const sliderHeight = 300;
const svgViewBoxHeight = 100;
const svgViewBoxThumbLimit = (sliderThumbSize / 2) * (svgViewBoxHeight / sliderHeight);
const svgViewBoxGraphMax = svgViewBoxHeight - svgViewBoxThumbLimit;
const svgViewBoxGraphMin = svgViewBoxThumbLimit;
let ranges = {
range1: null,
range2: null,
range3: null,
range4: null,
range5: null,
range6: null,
range7: null
};
// Only the y values changes
let points = {
begin: {
x: 10,
y: 0
},
point1: {
x: 10,
y: 0
},
control1: {
x: 20,
y: 10
},
control2: {
x: 20,
y: 0
},
point2: {
x: 30,
y: 0
},
control3: {
x: 40,
y: 0
},
point3: {
x: 50,
y: 0
},
control4: {
x: 60,
y: 0
},
point4: {
x: 70,
y: 0
},
control5: {
x: 80,
y: 0
},
point5: {
x: 90,
y: 0
},
control6: {
x: 100,
y: 0
},
point6: {
x: 110,
y: 0
},
control7: {
x: 120,
y: 0
},
point7: {
x: 130,
y: 0
},
};
function mapDataRange(value) {
// stackoverflow.com/a/929107/5707008
// return (((OldValue - OldMin) * (NewMax - NewMin)) / (OldMax - OldMin)) + NewMin
return (((value - 0) * (svgViewBoxGraphMax - svgViewBoxGraphMin)) / (svgViewBoxHeight - 0)) + svgViewBoxGraphMin;
}
function updateSlider($element) {
if ($element) {
let rangeIndex = $element.getAttribute('data-slider-index'),
range = ranges[rangeIndex],
value = $element.value;
if (range === value) {
return; // No value change, no need to update then
}
// Update state
ranges[rangeIndex] = value;
let parent = $element.parentElement,
$thumb = parent.querySelector('.range-slider__thumb'),
$bar = parent.querySelector('.range-slider__bar'),
pct = value * ((sliderHeight - sliderThumbSize) / sliderHeight)
$thumb.style.bottom = `${pct}%`;
$bar.style.height = `calc(${pct}% + ${sliderThumbSize/2}px)`;
//$thumb.textContent = `${value}%`;
renderSliderGraph();
}
}
function updatePoints() {
// Convert from percentage to coordinate values
// Calculate and floor the values
points.point1.y = svgViewBoxHeight - (svgViewBoxHeight * (ranges.range1) / 100) | 0;
points.point2.y = svgViewBoxHeight - (svgViewBoxHeight * (ranges.range2) / 100) | 0;
points.point3.y = svgViewBoxHeight - (svgViewBoxHeight * (ranges.range3) / 100) | 0;
points.point4.y = svgViewBoxHeight - (svgViewBoxHeight * (ranges.range4) / 100) | 0;
points.point5.y = svgViewBoxHeight - (svgViewBoxHeight * (ranges.range5) / 100) | 0;
points.point6.y = svgViewBoxHeight - (svgViewBoxHeight * (ranges.range6) / 100) | 0;
points.point7.y = svgViewBoxHeight - (svgViewBoxHeight * (ranges.range7) / 100) | 0;
const max = svgViewBoxGraphMax;
const min = svgViewBoxGraphMin;
points.point1.y = mapDataRange(points.point1.y);
points.point2.y = mapDataRange(points.point2.y);
points.point3.y = mapDataRange(points.point3.y);
points.point4.y = mapDataRange(points.point4.y);
points.point5.y = mapDataRange(points.point5.y);
points.point6.y = mapDataRange(points.point6.y);
points.point7.y = mapDataRange(points.point7.y);
// Update Y for the other points
points.begin.y = points.point1.y;
points.control1.y = points.point1.y;
points.control2.y = points.point2.y;
points.control3.y = points.point3.y;
points.control4.y = points.point4.y;
points.control5.y = points.point5.y;
points.control6.y = points.point6.y;
points.control7.y = points.point7.y;
}
function getInterpolatedLine(type) {
let shadowOffset = 0;
if (type === 'shadow') {
shadowOffset = 10; // simple simulation, no fancy shadow algorithm
}
return 'M {0},{1} L {2},{3} C {4},{5} {6},{7} {8},{9} S {10} {11}, {12} {13} S {14} {15}, {16} {17} S {18} {19}, {20} {21} S {22} {23}, {24} {25} S {26} {27}, {28} {29}'.format(
// M
points.begin.x, points.begin.y,
// L
points.point1.x, points.point1.y,
// C
points.control1.x, points.control1.y,
points.control2.x, points.control2.y + shadowOffset,
points.point2.x, points.point2.y + shadowOffset,
// S
points.control3.x, points.control3.y,
points.point3.x, points.point3.y,
// S
points.control4.x, points.control4.y + shadowOffset,
points.point4.x, points.point4.y + shadowOffset,
// S
points.control5.x, points.control5.y,
points.point5.x, points.point5.y,
// S
points.control6.x, points.control6.y + shadowOffset,
points.point6.x, points.point6.y + shadowOffset,
// S
points.control7.x, points.control7.y,
points.point7.x, points.point7.y,
)
}
function reset() {
const inputs = app.inputs;
inputs.forEach(input => input.value = 50);
inputs.forEach(input => app.updateSlider(input));
}
function renderSliderGraph() {
updatePoints();
$svgLine.setAttribute('d', getInterpolatedLine());
$svgLineShadow.setAttribute('d', getInterpolatedLine('shadow'));
}
function selectPreset(type) {
// Generate random graph
const inputs = app.inputs;
inputs.forEach(input => input.value = Math.random() * 100 | 0);
inputs.forEach(input => app.updateSlider(input));
}
return {
inputs: [].slice.call(document.querySelectorAll('.sliders input')),
updateSlider,
reset,
selectPreset,
};
})();
(function initAndSetupTheSliders() {
const inputs = app.inputs;
let index = 1;
inputs.forEach(input => input.setAttribute('data-slider-index', 'range' + index++));
inputs.forEach(input => input.value = 50);
inputs.forEach(input => app.updateSlider(input));
// Cross-browser support where value changes instantly as you drag the handle, therefore two event types.
inputs.forEach(input => input.addEventListener('input', element => app.updateSlider(input)));
inputs.forEach(input => input.addEventListener('change', element => app.updateSlider(input)));
app.selectPreset('custom');
})();
View Compiled
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.