DEV Community

Cover image for Creating Gooey Tooltips for Range Sliders using SVG Filters ✨
Utkarsh Verma
Utkarsh Verma

Posted on • Updated on

Creating Gooey Tooltips for Range Sliders using SVG Filters ✨

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.

GitHub logo 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

circleci npm minzipped size known vulnerabilities javascript style guide license

A lightweight (~2kB) library to create range sliders that can capture a value or a range of values with one or two drag handles.

Examples / CodePen

Demo

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

⚠️ It is recommended that you upgrade from v1.x to v2.x! What's new and what's changed in v2.x?



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';
Enter fullscreen mode Exit fullscreen mode

CDN

<script src="https://cdn.jsdelivr.net/npm/range-slider-input@2.4/dist/rangeslider.umd.min.js"></script>
Enter fullscreen mode Exit fullscreen mode

or

<script src="https://unpkg.com/range-slider-input@2"></script>
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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.

Positioning of thumb in <input type="range" />

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)`
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

FINAL RESULT

Thanks 🙂

Top comments (3)

Collapse
 
vulcanwm profile image
Medea

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?

Collapse
 
n3r4zzurr0 profile image
Utkarsh Verma • Edited

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.

Collapse
 
joelbonetr profile image
JoelBonetR 🥇

That's nice! 👌😁