Animating range ticks when moving range thumb
Asked Answered
N

1

3

I have custom input type="range", span that shows range's value and div with many p that act as ticks. I used custom div for ticks because appearance: none on the range hides the ticks. I am generating the ticks with DOM.

I've used slider thumb to act as a curved border and make the span with range value to act as the circle thumb. It cannot be curved where it connects with the slider but still works alright.


I want to achieve to animate the ticks to go over the thumb where it's placed. This is expected result:

expected result

This is my code and CodePen

// Creating ticks here to prevent long HTML code
var i;
const tickContainer = document.getElementById('tickContainer');

for (i = 1; i <= 100; i++) {
    var p = document.createElement('P');
    tickContainer.appendChild(p);
}

// Position of span that shows range value
const range = document.getElementById('range');
const rangeV = document.getElementById('rangeValue');
const setValue = () => {
  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>`;
};

// 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 {
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-box-pack: justify;
      -ms-flex-pack: justify;
          justify-content: space-between;
  position: absolute;
  width: 100%;
}

.ticks p {
  position: relative;
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-box-pack: center;
      -ms-flex-pack: center;
          justify-content: center;
  text-align: center;
  width: 1px;
  background: #D3D3D3;
  height: 10px;
  line-height: 40px;
  margin: 0 0 20px 0;
}

/* 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%, #0008d7 100%);
  outline: none;
}

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;
}

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%, #0008d7 100%);
  background-attachment: fixed, fixed;
  background-clip: padding-box, border-box;
}

input[type=range]::-moz-range-thumb {
  height: 70px;
  width: 70px;
  transform: rotate(45de);
  appearance: none;
  background: #ddd;
  border: 3px solid transparent;

  border-radius: 50%;
  cursor: pointer;
  background-image: linear-gradient(white, white), linear-gradient(to right, #e0e0e0 34%, #0008d7 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: #0008d7;
  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="1" step="1">
</div>
Nadianadine answered 1/7, 2020 at 0:9 Comment(1)
What have you tried to make the ticks go above it and didn't work?Pusillanimity
H
4

Here is an idea using mask. The trick is to create the ticks using background (to avoid a lot of element) then I use mask to create the curve shape around the thumb. The mask is composed by a radial-gradient (the circular shape that will move based on the thumb value) and a linear-gradient to create the base

// Creating ticks here to prevent long HTML code
var i;
const tickContainer = document.getElementById('tickContainer');

// Position of span that shows range value - not important
const range = document.getElementById('range');
const rangeV = document.getElementById('rangeValue');
const setValue = () => {
  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))`;
  tickContainer.style.setProperty('--p', `calc(${newValue}%)`);
  rangeV.innerHTML = `<span>${range.value}%</span>`;
};

// 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: -25px;
  height: 45px;
  background: repeating-linear-gradient(to right, #D3D3D3 0 2px, transparent 2px 6px);
  background-clip:content-box;
  -webkit-mask: 
    radial-gradient(farthest-side at bottom, #fff 98%, transparent) var(--p, 0) 0px/100px 50px, 
    linear-gradient(#fff, #fff) bottom/100% 10px;
  -webkit-mask-repeat: no-repeat;
  mask: 
    radial-gradient(farthest-side at bottom, #fff 98%, transparent) var(--p, 0) 0px/100px 50px, 
    linear-gradient(#fff, #fff) bottom/100% 10px;
  mask-repeat: no-repeat;
}


/* Styling of the range */

input[type=range] {
  -webkit-appearance: none;
  appearance: none;
  margin: 20px 0;
  width: 100%;
  background-image: linear-gradient(125deg, #e0e0e0 34%, #0008d7 100%);
  outline: none;
}

input[type=range]:focus {
  outline: none
}

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

input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  height: 70px;
  width: 70px;
  cursor: pointer;
  background: #ddd;
  /* Thumb gradient as slider */
  background-image: linear-gradient(white, white), linear-gradient(to right, #e0e0e0 34%, #0008d7 100%);
  background-attachment: fixed, fixed;
  background-clip: padding-box, border-box;
  /* Hide bottom half of the circle with white color - as background is */
  border: 3px solid transparent;
  border-color: transparent transparent #fff #fff;
  border-radius: 50%;
  transform: translateY(-44.3%) rotate(-45deg);
}


/* Range value (label) inside of range thumb */

.range-value {
  position: absolute;
  top: -50%;
  display: flex;
  justify-content: center;
  align-items: center;
}

.range-value span {
  width: 50px;
  height: 50px;
  line-height: 50px;
  text-align: center;
  color: #fff;
  background: #0008d7;
  font-size: 18px;
  display: block;
  position: absolute;
  top: 20px;
  border-radius: 50%;
  pointer-events: none;
  z-index: 99;
}
<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="1" step="1">
</div>

<p>If slider is not working propertly, your browser doesn't support <code>-webkit</code>. Visit <a href="https://codepen.io/Vepth/pen/zYrPZqv">CodePen</a> with full browser support</p>

To have the gap you can update the mask like below:

// Creating ticks here to prevent long HTML code
var i;
const tickContainer = document.getElementById('tickContainer');

// Position of span that shows range value - not important
const range = document.getElementById('range');
const rangeV = document.getElementById('rangeValue');
const setValue = () => {
  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))`;
  tickContainer.style.setProperty('--p', `calc(${newValue}%)`);
  rangeV.innerHTML = `<span>${range.value}%</span>`;
};

// 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 2px, transparent 2px 6px);
  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 of the range */

input[type=range] {
  -webkit-appearance: none;
  appearance: none;
  margin: 20px 0;
  width: 100%;
  background-image: linear-gradient(125deg, #e0e0e0 34%, #0008d7 100%);
  outline: none;
}

input[type=range]:focus {
  outline: none
}

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

input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  height: 70px;
  width: 70px;
  cursor: pointer;
  background: #ddd;
  /* Thumb gradient as slider */
  background-image: linear-gradient(white, white), linear-gradient(to right, #e0e0e0 34%, #0008d7 100%);
  background-attachment: fixed, fixed;
  background-clip: padding-box, border-box;
  /* Hide bottom half of the circle with white color - as background is */
  border: 3px solid transparent;
  border-color: transparent transparent #fff #fff;
  border-radius: 50%;
  transform: translateY(-44.3%) rotate(-45deg);
}


/* Range value (label) inside of range thumb */

.range-value {
  position: absolute;
  top: -50%;
  display: flex;
  justify-content: center;
  align-items: center;
}

.range-value span {
  width: 50px;
  height: 50px;
  line-height: 50px;
  text-align: center;
  color: #fff;
  background: #0008d7;
  font-size: 18px;
  display: block;
  position: absolute;
  top: 20px;
  border-radius: 50%;
  pointer-events: none;
  z-index: 99;
}
<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="1" step="1">
</div>

<p>If slider is not working propertly, your browser doesn't support <code>-webkit</code>. Visit <a href="https://codepen.io/Vepth/pen/zYrPZqv">CodePen</a> with full browser support</p>

Full code for all the browsers:

// Creating ticks here to prevent long HTML code
var i;
const tickContainer = document.getElementById('tickContainer');

// Position of span that shows range value - not important
const range = document.getElementById('range');
const rangeV = document.getElementById('rangeValue');
const setValue = () => {
  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))`;
  tickContainer.style.setProperty('--p', `calc(${newValue}%)`);
  rangeV.innerHTML = `<span>${range.value}%</span>`;
};

// 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 2px, transparent 2px 6px);
  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%, #0008d7 100%);
  outline: none;
}

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;
}

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;
  border-radius: 50%;
  cursor: pointer;
  background-image: linear-gradient(white, white), linear-gradient(to right, #e0e0e0 34%, #0008d7 100%);
  background-attachment: fixed, fixed;
  background-clip: padding-box, border-box;
}

input[type=range]::-moz-range-thumb {
  height: 63px;
  width: 63px;
  appearance: none;
  background: #ddd;
  border: 3px solid transparent;

  border-radius: 50%;
  cursor: pointer;
  background-image: linear-gradient(white, white), linear-gradient(to right, #e0e0e0 34%, #0008d7 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: #0008d7;
  font-size: 18px;
  display: block;
  position: absolute;
  top: 20px;
  border-radius: 50%;
  user-select: none;
  select: none;
  pointer-events: none;
  z-index: 100;
}

.range-value::after {
  content: '';
  position: absolute;
  width: 100px;
  height: 50px;
  top: 0;
  left: 0;
  background: white;
  user-select: none;
  select: none;
  pointer-events: none;
  transform: translate(-50%, 96%);
  -webkit-transform: translate(-50%, 92%);
}
<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="1" step="1">
</div>
Hinge answered 3/7, 2020 at 12:50 Comment(8)
Woah! That's so smart, thank you so much! Sadly, FireFox is having problems with it. Ticks start to go off if range is 80%+ and there is no gap between range thumb and ticks. One more thing, how did you use "Show code snippet" in your question?Classicize
@Vepthy you are having issue with firefox using the code here or after copying in your project? it's working fine for me .... for the "show code snippet" you can find this option on the bottom left when editing the snippet (an option to check)Hinge
I have merged your code with my CodePen that I used for cross browser compatibility. As you can see yourself, I didn't add any -moz and guided to CodePen because of code being too long. This is the merged CodePen. Also, thanks for that "Show code snippet", found it!Classicize
@Vepthy Will try to see how to adjust it for firefox but you can play with the value insdide mask without -webkit. those one are used by firefox. Chrome use the mask width -webkitHinge
I've been having problems with adjusting it for FireFox from the beginning. Seems like -moz is behaving differently. I would be grateful if you could fix it!Classicize
@Vepthy yes will try ;)Hinge
@Vepthy check this: jsfiddle.net/2cgoy3u9 if you reduce a little the range thumb it's betterHinge
Let us continue this discussion in chat so it doesn't become long to other people coming by.Classicize

© 2022 - 2024 — McMap. All rights reserved.