Connecting input range arc thumb to slider with curve
Asked Answered
F

1

5

I have a solved question Animating range ticks when moving range thumb. From that question, I have a input type="range" with custom appearance - thumb is made like an arc (half circle), span that takes range's value and is designed as a circled thumb and div with mask that act like ticks - steps.


From this expected result

expected result

I was trying to connect that arc to the slider with a curve. I tried to use pseudo elements but gradient doesn't sync with gradient on the slider and I cannot make a curve like shown in the picture. I also tried using JS canvas to draw that curve and place it on desired place but gradient again doesn't sync - becomes a stationary color.

I thought using a CSS mask but I'm not sure if it's possible to make wanted curve with that.


These are my main research points:

This is my CodePen and the code

// Position of span that shows range value and tick curve position
const tickContainer = document.getElementById('tickContainer');

const range = document.getElementById('range');
const rangeV = document.getElementById('rangeValue');
const setValue = () => {
  // Span position and inner value
  const newValue = Number((range.value - range.min) * 100 / (range.max - range.min));
  const newPosition = 35 - (newValue * 0.7);
  rangeV.style.left = `calc(${newValue}% + (${newPosition}px))`;
  rangeV.innerHTML = `<span>${range.value}%</span>`;
  
  // Tick curve position
  tickContainer.style.setProperty('--p', `calc(${newValue}%)`);
};

// Initialize setValue onload and oninput
document.addEventListener("DOMContentLoaded", setValue);
range.addEventListener('input', setValue);
body {
  font-family: Arial;
  margin: 50px;
}

.range-wrap {
  position: relative;
}

/* Styling of ticks (lines) over the range */
.ticks {
  position: absolute;
  left: -15px;
  right: -15px;
  padding:0 15px;
  top: -30px;
  height: 45px;
  background: repeating-linear-gradient(to right, #D3D3D3 0 1px, transparent 1px 9px);
  background-clip:content-box;
  -webkit-mask: 
    radial-gradient(farthest-side at bottom,transparent 75%, #fff 76% 98%, transparent) 
      var(--p) 0px/100px 50px, 
    linear-gradient(#fff, #fff) var(--p) 100%/95px 10px,
    linear-gradient(#fff, #fff) bottom       /100% 10px;
  -webkit-mask-repeat: no-repeat;
  -webkit-mask-composite: source-over,destination-out;
  mask: 
    radial-gradient(farthest-side at bottom,transparent 75%, #fff 76% 98%, transparent) 
      var(--p) 0px/100px 50px, 
    linear-gradient(#fff, #fff) var(--p) 100%/95px 10px,
    linear-gradient(#fff, #fff) bottom       /100% 10px;
  mask-repeat: no-repeat;
  mask-composite: exclude;
}

/* Styling the range */
input[type=range] {
  -webkit-appearance: none;
  appearance: none;
  margin: 20px 0;
  width: 100%;
  height: 4px;
  background-image: linear-gradient(125deg, #e0e0e0 34%, rgb(0,12,110) 100%);
  outline: none;
  transition: all 100ms ease;
}

/* Range track */
input[type=range]::-webkit-slider-runnable-track {
  width: 100%;
  height: 4px;
  cursor: pointer;
  border-radius: 25px;
}

input[type=range]::-moz-range-track {
  width: 100%;
  height: 4px;
  cursor: pointer;
  border-radius: 25px;
}

/* Range thumb */
input[type=range]::-webkit-slider-thumb {
  height: 70px;
  width: 70px;
  -webkit-transform: translateY(-44.3%) rotate(-45deg);
          transform: translateY(-44.3%) rotate(-45deg);
  -webkit-appearance: none;
  appearance: none;
  background: #ddd;
  border: 3px solid transparent;
  border-color: transparent transparent #fff #fff;
  border-radius: 50%;
  cursor: pointer;
  background-image: linear-gradient(white, white), linear-gradient(to right, #e0e0e0 34%, rgb(0,12,110) 100%);
  background-attachment: fixed, fixed;
  background-clip: padding-box, border-box;
  transition: all 200ms ease;
}

input[type=range]::-moz-range-thumb {
  height: 63px;
  width: 63px;
  appearance: none;
  background: #ddd;
  border: 3px solid transparent;
  transition: all 200ms ease;
  border-color: transparent transparent #fff #fff;
  border-radius: 50%;
  cursor: pointer;
  background-image: linear-gradient(white, white), linear-gradient(to right, #e0e0e0 34%, rgb(0,12,110) 100%);
  background-attachment: fixed, fixed;
  background-clip: padding-box, border-box;
}

/* Range value (label) inside of range thumb */
.range-value {
  position: absolute;
  top: -50%;
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-box-pack: center;
      -ms-flex-pack: center;
          justify-content: center;
  -webkit-box-align: center;
      -ms-flex-align: center;
          align-items: center;
  z-index: 99;
  user-select: none;
  select: none;
  pointer-events: none;
}

.range-value span {
  width: 50px;
  height: 50px;
  line-height: 50px;
  text-align: center;
  color: #fff;
  background: rgb(0,12,110);
  font-size: 18px;
  display: block;
  position: absolute;
  top: 20px;
  border-radius: 50%;
  user-select: none;
  select: none;
  pointer-events: none;
  z-index: 100;
}
<div class="range-wrap">
  <!-- Ticks (lines) over slider -->
  <div class="ticks" id="tickContainer">
  </div>
  <!-- Range value inside of range thumb -->
  <div class="range-value" id="rangeValue"></div>
  <!-- Range itself -->
  <input id="range" type="range" min="1" max="100" value="5" step="1">
</div>
Filthy answered 3/7, 2020 at 22:4 Comment(0)
D
6

Here is different idea where I will rely on mask like my previous answer but this time I will introduce and SVG for the curved part. I will also optimize the code a little to have less of code.

You will notice that I am using the same mask for both the ticks and the range element but with some different values since the ticks need to have a bigger curve.

// Position of span that shows range value and tick curve position
const tickContainer = document.querySelector('.range-wrap');

const range = document.getElementById('range');
const rangeV = document.getElementById('rangeValue');
const setValue = () => {
  // Span position and inner value
  const newValue = Number((range.value - range.min) * 100 / (range.max - range.min));
  const newPosition = 30 - (newValue * 0.6);
  rangeV.style.left = `calc(${newValue}% + (${newPosition}px))`;
  rangeV.innerHTML = `${range.value}%`;
  
  // Tick curve position
  tickContainer.style.setProperty('--p', `calc(${newValue}%)`);
};

// Initialize setValue onload and oninput
document.addEventListener("DOMContentLoaded", setValue);
range.addEventListener('input', setValue);
body {
  font-family: Arial;
  margin: 50px;
}

.range-wrap {
  position: relative;
  --svg:url("data:image/svg+xml;utf8, <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 15 64 50' preserveAspectRatio='none' ><path d='M0 64 C16 64 16 32 32 32 C48 32 48 64 64 64 L64 48 C52 48 52 16 32 16 C12 16 12 48 0 48 L0 64 Z' fill='white'/></svg>") var(--p,0) 0;
}

/* Styling of ticks (lines) over the range */
.ticks {
  --sw:120px; /* control the width of the curve */
  
  position: absolute;
  left: -30px;
  right: -30px;
  padding:0 10px;
  height: 50px;
  background: repeating-linear-gradient(to right, red 0 3px, transparent 1px 9px) content-box;
  -webkit-mask:var(--svg) /var(--sw) 50px,
     linear-gradient(to right, #fff calc(50% - var(--sw)/2 + 1px), transparent 0 calc(50% + var(--sw)/2 - 1px), #fff 0) 
     right var(--p) top 33px/calc(200% - var(--sw)) 16px;
  -webkit-mask-repeat:no-repeat;
  z-index:999;
}

/* Styling the range */
input[type=range] {
  --sw:100px; /* control the width of the curve */
  
  -webkit-appearance: none;
  appearance: none;
  margin: 20px 0 20px -20px;
  padding:0 20px;
  width:100%;
  height: 90px;
  -webkit-mask: 
    var(--svg) /var(--sw) 50px,
    linear-gradient(to right, #fff calc(50% - var(--sw)/2 + 1px), transparent 0 calc(50% + var(--sw)/2  - 1px), #fff 0) 
    right var(--p) top 33px/calc(200% - var(--sw)) 16px;
  -webkit-mask-repeat:no-repeat;
  background: linear-gradient(125deg, #e0e0e0 34%, rgb(0,12,110) 100%);
  outline: none;
}

/* Range track */
input[type=range]::-webkit-slider-runnable-track {
  width: 100%;
  height: 50px;
  cursor: pointer;
  border-radius: 25px;
}

input[type=range]::-moz-range-track {
  width: 100%;
  height: 50px;
  cursor: pointer;
  border-radius: 25px;
}

/* Range thumb */
input[type=range]::-webkit-slider-thumb {
  height: 60px;
  width: 60px;
  -webkit-appearance: none;
  appearance: none;
  border-radius: 50%;
  cursor: pointer;
  opacity:0;
}

input[type=range]::-moz-range-thumb {
  height: 60px;
  width: 60px;
  appearance: none;
  border-radius: 50%;
  cursor: pointer;
  opacity:0;
}

/* Range value (label) inside of range thumb */
.range-value {
  width: 50px;
  height: 50px;
  line-height: 50px;
  text-align: center;
  color: #fff;
  background: rgb(0,12,110);
  font-size: 18px;
  position: absolute;
  transform:translateX(-50%);
  top: 45px;
  border-radius: 50%;
  user-select: none;
  select: none;
  pointer-events: none;
}
<div class="range-wrap">
  <!-- Ticks (lines) over slider -->
  <div class="ticks" id="tickContainer">
  </div>
  <!-- Range value inside of range thumb -->
  <div class="range-value" id="rangeValue"></div>
  <!-- Range itself -->
  <input id="range" type="range" min="1" max="100" value="5" step="1">
</div>

UPDATE

The final version used by the OP:

// Position of span that shows range value and tick curve position
const tickContainer = document.querySelector('.range-wrap');

const range = document.getElementById('range');
const rangeV = document.getElementById('rangeValue');
const setValue = () => {
  // Span position and inner value
  const newValue = Number((range.value - range.min) * 100 / (range.max - range.min));
  const newPosition = 30 - (newValue * 0.6);
  rangeV.style.left = `calc(${newValue}% + (${newPosition}px))`;
  rangeV.innerHTML = `${range.value}%`;
  
  // Tick curve position
  tickContainer.style.setProperty('--p', `calc(${newValue}%)`);
};

// Initialize setValue onload and oninput
document.addEventListener("DOMContentLoaded", setValue);
range.addEventListener('input', setValue);
body {
  font-family: Arial;
  margin: 0;
  min-height: 100vh;
  padding: 50px;
  box-sizing: border-box;
  text-align:center;
}

.range-wrap {
  position: relative;
  --svg:url("data:image/svg+xml;utf8, <svg width='97' height='37' viewBox='0 1.5 97 37' xmlns='http://www.w3.org/2000/svg'><path d='M0 35C14 35 13 2 48.5 2C84 2 80.5 35 97 35' fill='none' stroke='white' stroke-width='4'/></svg>") var(--p,0) 0;
</svg>
}

/* Styling of ticks (lines) over the range */
.ticks {
  --sw:120px; /* control the width of the curve */
  
  position: absolute;
  left: -30px;
  right: -30px;
  top: 0px;
  padding:0 10px;
  height: 50px;
  background: repeating-linear-gradient(to right, #D3D3D3 0 1px, transparent 1px 10px) content-box;
  -webkit-mask:var(--svg) /var(--sw) 50px,
     linear-gradient(to right, #fff calc(50% - var(--sw)/2 + 1px), transparent 0 calc(50% + var(--sw)/2 - 1px), #fff 0) 
     right var(--p) top 38px/calc(200% - var(--sw)) 6px;
  -webkit-mask-repeat:no-repeat;
  z-index:999;
}

/* Styling the range */
input[type=range] {
  --sw:100px; /* control the width of the curve */
  
  -webkit-appearance: none;
  appearance: none;
  margin: 20px 0 20px -20px;
  padding:0 20px;
  width: 100%;
  height: 60px;
  -webkit-mask: 
    var(--svg) /var(--sw) 41px,
    linear-gradient(to right, #fff calc(50% - var(--sw)/2 + 1px), transparent 0 calc(50% + var(--sw)/2  - 1px), #fff 0) 
    right var(--p) top 34.45px/calc(200% - var(--sw)) 4px;
  -webkit-mask-repeat:no-repeat;
  background: linear-gradient(125deg, #e0e0e0 34%, rgb(0,12,110) 100%);
  outline: none;
}

/* Range track */
input[type=range]::-webkit-slider-runnable-track {
  width: 100%;
  height: 50px;
  cursor: pointer;
  border-radius: 25px;
}

input[type=range]::-moz-range-track {
  width: 100%;
  height: 50px;
  cursor: pointer;
  border-radius: 25px;
}

/* Range thumb */
input[type=range]::-webkit-slider-thumb {
  height: 60px;
  width: 60px;
  -webkit-appearance: none;
  appearance: none;
  border-radius: 50%;
  cursor: pointer;
  opacity:0;
}

input[type=range]::-moz-range-thumb {
  height: 60px;
  width: 60px;
  appearance: none;
  border-radius: 50%;
  cursor: pointer;
  opacity:0;
}

/* Range value (label) inside of range thumb */
.range-value {
  width: 55px;
  height: 55px;
  line-height: 60px;
  text-align: center;
  color: #fff;
  background: rgb(0,12,110);
  font-size: 18px;
  position: absolute;
  transform:translateX(-50%);
  top: 32px;
  border-radius: 50%;
  user-select: none;
  select: none;
  pointer-events: none;
}
<h2>Custom range slider with ticks</h2>

<div class="range-wrap">
  <!-- Ticks (lines) over slider -->
  <div class="ticks" id="tickContainer">
  </div>
  <!-- Range value inside of range thumb -->
  <div class="range-value" id="rangeValue"></div>
  <!-- Range itself -->
  <input id="range" type="range" min="1" max="100" value="5" step="1">
</div>
Disforest answered 3/7, 2020 at 23:39 Comment(6)
Nice answer. Though, is there a possibility to change the arc and slider thickness to 4px as it was originally or to any other thickness in general?Sherronsherry
@Vepthy yes you can, you have to play with the mask size and other values. Will try to edit to show how it can be done but you can start playing with the value and you will understandDisforest
Also if I were to make ticks lower or make gap between them and slider bigger, what should I control with the curved ticks to have the same space? This is what I've been trying. CodePenSherronsherry
I've tried lowering the height of the slider from 50px to 4px and range itself but it just disappears behind the ticks.Sherronsherry
@Vepthy no, it's as simply as changing the height, you need to change a lot of values inside the mask, the heights need to remain untouched. For the gaps, add to the ticks ` top: -2px;` for example and play with it to control the gapDisforest
Ah alright, I get it for the gap now. Now I just need to try changing the thickness.Sherronsherry

© 2022 - 2024 — McMap. All rights reserved.