A few weeks ago, I developed a small library for creating range sliders that can capture a value or a range of values with one or two drag handles.
In this article, I will use it to create range sliders and then create gooey tooltips for them using SVG filters.
n3r4zzurr0 / range-slider-input
A lightweight (~2kB) library to create range sliders that can capture a value or a range of values with one or two drag handles
range-slider-input
A lightweight (~2kB) library to create range sliders that can capture a value or a range of values with one or two drag handles.
✨ Features
- High CSS customizability
- Touch and keyboard accessible
- Supports negative values
- Vertical orientation
- Small and fast
- Zero dependencies
- Supported by all major browsers
- Has a React component wrapper
Installation
npm
npm install range-slider-input
Import the rangeSlider
constructor and the core CSS:
import rangeSlider from 'range-slider-input';
import 'range-slider-input/dist/style.css';
CDN
<script src="https://cdn.jsdelivr.net/npm/range-slider-input@2.4/dist/rangeslider.umd.min.js"></script>
or
<script src="https://unpkg.com/range-slider-input@2"></script>
The core CSS comes bundled with the jsDelivr and unpkg imports.
Usage
import rangeSlider from 'range-slider-input';
import 'range-slider-input/dist/style.css';
const rangeSliderElement = rangeSlider(element);
API
rangeSlider(element, options =
…I have divided this article into 4 sections. Let's begin (or just jump to the final result)!
1. Creating the Range Sliders
Here, I have created two range sliders, one with a single thumb and the other with two thumbs, and have styled them with some custom CSS (refer to the documentation to know more about styling).
So, this is going to be our starting point.
2. Creating the Gooey Effect
Gooey effect can be obtained by applying a blur filter, and by increasing the contrast later, which can be done using the CSS property filter
.
filter: blur(6px) contrast(20);
But, there are a few drawbacks to this approach, the worst one being that it falls back to the nearest standard color which restricts the range of the colors that can be used.
This is where the SVG filters are very helpful. For our case, the filter should look like:
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
<defs>
<filter id="gooey">
<feGaussianBlur in="SourceGraphic" stdDeviation="6" result="blur" />
<feColorMatrix in="blur" mode="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 18 -6" />
</filter>
</defs>
</svg>
Here, the feGaussianBlur
filter effect applies the blur filter, and to increase the contrast, we use the feColorMatrix
filter effect which involves manipulation of color channels. Here is an extensive article on this filter effect: Finessing feColorMatrix, if you want to dig deeper into this subject.
For our case, we just need to increase the contrast of the alpha channel, so we will leave the RGB channels untouched.
R | G | B | A | + | |
---|---|---|---|---|---|
R | 1 | 0 | 0 | 0 | 0 |
G | 0 | 1 | 0 | 0 | 0 |
B | 0 | 0 | 1 | 0 | 0 |
A | 0 | 0 | 0 | 18 | -6 |
With this manipulation, the value of the alpha channel gets multiplied by 18 and then (255 * 6) gets subtracted from it, effectively increasing the contrast of the transparency. You can play with these values until you get your desired result.
3. Positioning of blobs over thumbs
After creating the desired gooey effect, we will need to place the blobs over the thumbs of the range sliders.
Create a container element with position
set to relative
. Inside it, put the slider and the blobs with their position
set to absolute
and update the left
positional property of the blobs whenever the value of the slider is changed. But, what should we update the left
property with? Value percentage? No!
Let's see why.
These range sliders are in accordance with <input type="range" />
, so the position of the thumbs isn't simply the value percentage. The width of the thumb is taken into account too.
So, in order to calculate the position of a thumb, we will do
const thumbWidth = 30 // 30px for example
const fraction = (value - MIN) / (MAX - MIN)
blobs.style.left = `calc(${fraction * 100}% + ${(0.5 - fraction) * thumbWidth}px)`
where MIN
and MAX
are the min
and max
properties of the range slider, which are 0
and 100
by default respectively.
4. Putting everything together
Our final HTML structure looks something like:
<div class="container">
<div class="slider"></div>
<div class="blobs">
<div class="blob"></div>
<div class="blob"></div>
</div>
<div class="value-text"></div>
</div>
Also, to make the code more readable, I have written a wrapper function that creates a range slider, pops up one of the blobs upon user interaction, and updates the position of the blobs whenever there is a change in the value.
function gooeyRangeSlider (element, options = {}, initialIndex = 0) {
const slider = element.querySelector('.slider')
const blobs = element.querySelector('.blobs')
const valueText = element.querySelector('.value-text')
let value = []
// initialIndex denotes the index of the thumb over which the blobs have to be placed initially
const currentIndex = () => {
// currentValueIndex returns -1 when the thumbs are in idle state
const index = wrapper.currentValueIndex()
return index === -1 ? initialIndex : index
}
const update = () => {
const index = currentIndex()
const fraction = (value[index] - wrapper.min()) / (wrapper.max() - wrapper.min())
const left = `calc(${fraction * 100}% + ${(0.5 - fraction) * 30}px)`
blobs.style.left = left
valueText.style.left = left
valueText.textContent = value[index]
}
const wrapper = rangeSlider(slider, {
...options,
onInput: v => {
value = v
update()
},
onThumbDragStart: () => {
blobs.classList.add('active')
update()
},
onThumbDragEnd: () => {
blobs.classList.remove('active')
}
})
value = wrapper.value()
update()
}
FINAL RESULT
Thanks 🙂
Top comments (3)
This is really good!
I noticed that in the cover gif, there is a different font to the one in the final result.
What font did you use in the cover gif?
Poppins
I guess it's cached in my browser so I forget to include it explicitly every time.
Have updated it in the final result too.
That's nice! 👌😁