Animating range tick movements as the range thumb moves

My custom range input includes a span that displays the range's value and a div with multiple p elements acting as ticks. To create these ticks, I had to use a custom div because using "appearance: none" on the range hides the ticks by default. The tick elements are generated in the DOM.

I have styled the slider thumb to have a curved border, and the span displaying the range value acts as a circular thumb. While the connection between the circle thumb and the slider cannot be curved, it still functions correctly.


The desired outcome is to animate the ticks so they move over the thumb when positioned. This image demonstrates the expected result:

https://i.stack.imgur.com/BEn4E.png

This is the relevant code snippet and accompanying CodePen demo link:

// JavaScript code for generating ticks dynamically
var i;
const tickContainer = document.getElementById('tickContainer');

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

// JS function to position the span displaying 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>`;
};

// Call the setValue function onload and oninput
document.addEventListener("DOMContentLoaded", setValue);
range.addEventListener('input', setValue);
CSS styles go here...
HTML structure goes here...

Answer №1

Using the mask, a unique idea is implemented here to create ticks with background instead of elements and then shape a curve around the thumb using masks. The mask consists of a radial-gradient for the circular shape that moves based on the thumb value, and a linear-gradient to establish the foundation.

// To avoid lengthy HTML code, ticks are created here
var i;
const tickContainer = document.getElementById('tickContainer');

// Positioning the range value span - not crucial
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>`;
};

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

.range-wrap {
  position: relative;
}


/* Styling the ticks 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 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 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 properly, it means your browser doesn't support <code>-webkit</code>. You can visit <a href="https://codepen.io/Vepth/pen/zYrPZqv">CodePen</a> for full browser support.</p>

To introduce a gap, you can update the mask in this way:

// 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 does not work correctly, it may be due to lack of support for <code>-webkit</code> in your browser. Consider visiting <a href="https://codepen.io/Vepth/pen/zYrPZqv">CodePen</a> for complete browser support.</p>


Here's the entire code compatible across all browsers:

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

// Position of span that shows range value - not crucial
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>`;
};

// Initializing 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>

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

VUE JS - My methods are triggering without any explicit invocation from my side

I've encountered a frustrating problem with Vue JS >.<. My methods are being triggered unexpectedly. I have a button that is supposed to execute a specific method, but this method gets executed along with other methods, causing annoyance... Her ...

Is there an issue with Vue-router 2 where it changes the route but fails to update the view

I am currently facing an issue with the login functionality on a website that utilizes: Vue.js v2.0.3 vue-router v2.0.1 vuex v0.8.2 In routes.js, there is a basic interceptor setup router.beforeEach((to, from, next) => { if (to.matched.some(record ...

stop an html page from opening a new window

When using my HTML-based application, there are instances when it needs to open another URL within an iframe. However, a problem arises when the third-party URL within the iframe also opens a new window with the same content and URL. How can I prevent th ...

Is it possible to create multiple text input components using the "each" function, and how can I update the state by combining all of them together?

I am looking to create a text-based word game where the length of each word changes with every level. Each letter will be placed in its own box, forming a matrix (e.g. 10 words, length: 10 => 10x10 matrix). How can I generate multiple text input componen ...

What is the best way to display an entire string in a DataGridPro cell without truncating it with an ellipsis?

After reviewing all of the available DataGrid documentation, I am still unable to find a solution for displaying strings in multiple lines within a cell without ellipses. The current behavior is as follows: https://i.stack.imgur.com/TO8vB.png What I am a ...

What is the best way to define a variable that captures and logs the values from multiple input variables?

Hey there, I'm a new coder working on a shopping list app. I'm trying to display the input from three fields - "item", "store", and "date" - at the bottom of the page as a single line item. I attempted to do this by creating a variable called "t ...

Leveraging the power of Google Closure Templates alongside the versatility of

We are embarking on developing an application using JavaScript and HTML5 that will utilize a rest API to access server resources, leveraging the power and convenience of jQuery which our development team is already proficient in. Our goal is to make this a ...

Transitioning between sections by clicking on a navigation menu item

I'm looking to create a cool animation effect on my website. When a user clicks on a specific menu link, such as "about-section," I want the page to smoothly scroll down to that section in a parallax style. If anyone knows of a jQuery plugin that cou ...

Scrollbar appearing in Safari (Windows) despite increasing the height

While working on a website for a client, I encountered an issue where a section within the footer is causing scrollbars to appear. Despite adjusting the height of the wrapper and setting overflow to hidden, the scrollbars persist. Is there anyone who can ...

Converting Hexadecimal Values to Base32-Encoding Using Javascript

I'm encountering a problem with converting a function from Ruby to Javascript (specifically node.js, but I prefer a solution that is compatible with browsers, if possible). Here is the hex-formatted sha256 digest: "0b08dfe80a49490ae0722b9306ff53c5ab ...

Using jQuery to append an <option> element to a <select> tag

Every time I try to add an option to a select, the option I want to add gets appended to the first option instead of the actual select element. $(".ct [value='']").each(function() { $(this).append($("<option></option>").attr("val ...

Listcell XUL with button options

How can I make buttons inside a listcell function in XUL? I am having trouble getting it to work. Here is the XUL code: <listitem id = "1"> <listcell label = "OK Computer"/> <listcell label = "Radiohead"/> <listcell label ...

Conceal the Slider until Fully Loaded with Slick Slider by Ken Wheeler

I am currently using a slider function called Slick by Ken Wheeler. It is currently being loaded in the footer with just two variables. $('.slickSlider').slick({ dots: true, fade: true }); Currently, ...

Issue with flexbox max-width property not being applied when resizing the page width

I'm struggling to make a flex box with max and min width properties work properly. When I reduce the page size, it ends up showing blank space instead of resizing. How can I troubleshoot this issue? .parent{ border: 1px solid red; width: 50%; ...

What is the process for dynamically setting @click in vue.js?

My array contains objects with methods as actions from a vue object. How can I dynamically set the @click event in a v-for loop? I attempted to use this.m1, "this.m1", "m1", but encountered an error: fns.apply is not a function. Javascript: new Vue ...

What is the best way to verify a user's login status in AngularJS using $routeChangeStart event?

I am new to AngularJS and I need help checking if my user is logged in or not using $routeChangeStart. Controller angular.module('crud') .controller('SigninCtrl', function ($scope,$location,User,$http) { $scope.si ...

Can Backbone be used to retrieve a local JSON file?

I have a simple and validated local JSON file that I am trying to fetch using Backbone.js. Here is the snippet of Backbone.js code: MyModel = Backbone.Model.extend({ defaults:{ name: '', age: 0 } }); MyCollection =Backbo ...

The instance of my ObjectType is coming back as an empty entity

Having trouble making relationships between two object types in my code. One of them is working fine, but the other one returns an empty object and I can't seem to find the issue. The first one works as expected and logs the rank type without any pro ...

The Bootstrap DateTimePicker is displaying on the screen in an inaccurate

I'm having an issue with the datetimepicker on my project. The calendar appears incorrectly in Chrome, but looks fine in Edge. Here's a screenshot from Chrome: https://i.stack.imgur.com/Hi0j4.png And here is how it looks in Edge: https://i.stac ...

Click the edit button to access the options in the material table

https://i.stack.imgur.com/Inyow.png Currently, I am utilizing Material Table within Reactjs to display the table Data. However, I have encountered a hurdle where I need to alter state upon clicking on the edit option/icon. My objective is not to modify th ...