Creating Gooey Tooltips for Range Sliders using SVG Filters ✨
Tags: webdev, javascript, css, svg
Published: 2022-06-06 05:26:27

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.

{% embed https://github.com/n3r4zzurr0/range-slider-input %}

I have divided this article into 4 sections. Let's begin (or just **[jump to the final result](#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](https://github.com/n3r4zzurr0/range-slider-input/blob/main/README.md#styling) to know more about styling).

So, this is going to be our starting point.

{% codepen https://codepen.io/n3r4zzurr0/pen/yLvqVbP %}

## 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`.

```css
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](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/filter) are very helpful. For our case, the filter should look like:

```html
<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](https://alistapart.com/article/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.

{% codepen https://codepen.io/n3r4zzurr0/pen/oNEMzOa %}

## 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" />](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vcuhyadebb27xyidz9o1.png)

So, in order to calculate the position of a thumb, we will do

```js
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.

{% codepen https://codepen.io/n3r4zzurr0/pen/GRQBrZm %}

## 4. Putting everything together

Our final HTML structure looks something like:

```html
<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.

```js
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

{% codepen https://codepen.io/n3r4zzurr0/pen/NWyBRpQ %}

**Thanks** 🙂