Tutorial

How To Build an Elastic Range Input with SVG and anime.js

Published on December 12, 2019
author

Luis Manuel

How To Build an Elastic Range Input with SVG and anime.js

Introduction

In HTML5, many new types of input attributes were introduced for the form element, such as color, date, range, and many more. Although functionally these new types of input works, they often do not meet the aesthetic needs of web applications.

To give these input types a modern design, one can use such front, this tutorial will simulate the behavior of a range input with a component using SVG to draw the path and anime.js to perform the animations.

Once you follow this tutorial, you will know the essential steps to creating a range input design like the following:

Range input design by Stan Yakusevich

Note: This original animation, which we have used as inspiration, can be found on this dribble shot by Stan Yakusevich.

If you would like to see the final product, check it out on CodePen.

Step 1 — Coding the Markup with HTML and SVG

In this first step, we will see the main HTML structure that we will use. Please read the comments so you do not miss a single detail:

<!-- Wrapper for the range input slider -->
<div class="range__wrapper">
    <!-- The real input will be hidden, but updated properly with Javascript -->
    <!-- For a production usage, you may want to add a label and also put it inside a form -->
    <input class="range__input" type="range" min="30" max="70" value="64"/>

    <!-- All the other elements will go here -->
</div>

As we can see, our component contains an actual input of type range, which we will update properly with Javascript. Having this input element and our component in a common HTML form allows us to send the value of the input (along with the other form data) to the server on submit.

Now let’s see the SVG elements that we need, commented for a better understanding:

<!-- SVG elements -->
<svg class="range__slider" width="320px" height="480px" viewBox="0 0 320 480">
    <defs>
        <!-- Range marks symbol. It will be reused below -->
        <symbol id="range__marks" shape-rendering="crispEdges">
            <path class="range__marks__path" d="M 257 30 l 33 0"></path>
            <path class="range__marks__path" d="M 268 60 l 22 0"></path>
            <path class="range__marks__path" d="M 278 90 l 12 0"></path>
            <path class="range__marks__path" d="M 278 120 l 12 0"></path>
            <path class="range__marks__path" d="M 278 150 l 12 0"></path>
            <path class="range__marks__path" d="M 278 180 l 12 0"></path>
            <path class="range__marks__path" d="M 278 210 l 12 0"></path>
            <path class="range__marks__path" d="M 278 240 l 12 0"></path>
            <path class="range__marks__path" d="M 278 270 l 12 0"></path>
            <path class="range__marks__path" d="M 278 300 l 12 0"></path>
            <path class="range__marks__path" d="M 278 330 l 12 0"></path>
            <path class="range__marks__path" d="M 278 360 l 12 0"></path>
            <path class="range__marks__path" d="M 278 390 l 12 0"></path>
            <path class="range__marks__path" d="M 268 420 l 22 0"></path>
            <path class="range__marks__path" d="M 257 450 l 33 0"></path>
        </symbol>
        <!-- This clipPath element will allow us to hide/show the white marks properly -->
        <!-- The `path` used here is an exact copy of the `path` used for the slider below -->
        <clipPath id="range__slider__clip-path">
            <path class="range__slider__path" d="M 0 480 l 320 0 l 0 480 l -320 0 Z"></path>
        </clipPath>
    </defs>
    <!-- Pink marks -->
    <use xlink:href="#range__marks" class="range__marks__pink"></use>
    <!-- Slider `path`, that will be morphed properly on user interaction -->
    <path class="range__slider__path" d="M 0 480 l 320 0 l 0 480 l -320 0 Z"></path>
    <!-- Clipped white marks -->
    <use xlink:href="#range__marks" class="range__marks__white" clip-path="url(#range__slider__clip-path)"></use>
</svg>

Note: If this is the first time you use the SVG path element or you don’t understand how they work, you can learn more in this tutorial from Mozilla.

Finally, we need another piece of code to show the values and texts that appear in the original animation:

<!-- Range values -->
<div class="range__values">
    <div class="range__value range__value--top">
        <!-- This element will be updated in the way: `100 - inputValue` -->
        <span class="range__value__number range__value__number--top"></span>
        <!-- Some text for the `top` value -->
        <span class="range__value__text range__value__text--top">
            <span>Points</span>
            <span>You Need</span>
        </span>
    </div>
    <div class="range__value range__value--bottom">
        <!-- This element will be updated with the `inputValue` -->
        <span class="range__value__number range__value__number--bottom"></span>
        <!-- Some text for the `bottom` value -->
        <span class="range__value__text range__value__text--bottom">
            <span>Points</span>
            <span>You Have</span>
        </span>
    </div>
</div>

Now let’s look at the styles.

Step 2 — Adding Styles

We will start styling the wrapper element:

.range__wrapper {
  user-select: none; // disable user selection, for better drag & drop

  // More code for basic styling and centering...
}

As you can see, apart from the basic styles to achieve a proper appearance and centering the element, we have disabled the user’s ability to select anything within our component. This is important, since we will implement a “drag and drop” type interaction, and therefore if we allow the “select” functionality, we can get unexpected behaviors.

Next we will hide the actual input element, and position the svg (.range__slider) element properly:

// Hide the `input`
.range__input {
  display: none;
}

// Position the SVG root element
.range__slider {
  position: absolute;
  left: 0;
  top: 0;
}

To color the SVG elements we use the following code:

// Slider color
.range__slider__path {
  fill: #FF4B81;
}

// Styles for marks
.range__marks__path {
  fill: none;
  stroke: inherit;
  stroke-width: 1px;
}

// Stroke color for the `pink` marks
.range__marks__pink {
  stroke: #FF4B81;
}

// Stroke color for the `white` marks
.range__marks__white {
  stroke: white;
}

Now let’s see the main styles used for the values. Here the transform-origin property plays an essential role to keep the numbers aligned with the text in the desired way, as in the original animation.

// Positioning the container for values; it will be translated with Javascript
.range__values {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
}

// These `transform-origin` values will keep the numbers in the desired position as they are scaled
.range__value__number--top {
  transform-origin: 100% 100%; // bottom-right corner
}
.range__value__number--bottom {
  transform-origin: 100% 0; // top-right corner
}

// More basic styles for the values...

Step 3 — Adding Interactions with JavaScript

Now it’s time to add the interactions and start animating things.

First, let’s see the code needed for simulating the drag and drop functionality, listening to corresponding events, doing math work, and performing animations. Please note we are not including the whole code, but only the fundamental parts to understand the behavior.

// Handle `mousedown` and `touchstart` events, saving data about mouse position
function mouseDown(e) {
    mouseY = mouseInitialY = e.targetTouches ? e.targetTouches[0].pageY : e.pageY;
    rangeWrapperLeft = rangeWrapper.getBoundingClientRect().left;
}

// Handle `mousemove` and `touchmove` events, calculating values to morph the slider `path` and translate values properly
function mouseMove(e) {
    if (mouseY) {
        // ... Some code for maths ...
        // After doing maths, update the value
        updateValue();
    }
}

// Handle `mouseup`, `mouseleave`, and `touchend` events
function mouseUp() {
    // Trigger elastic animation in case `y` value has changed
    if (mouseDy) {
        elasticRelease();
    }
    // Reset values
    mouseY = mouseDy = 0;
}

// Events listeners
rangeWrapper.addEventListener('mousedown', mouseDown);
rangeWrapper.addEventListener('touchstart', mouseDown);
rangeWrapper.addEventListener('mousemove', mouseMove);
rangeWrapper.addEventListener('touchmove', mouseMove);
rangeWrapper.addEventListener('mouseup', mouseUp);
rangeWrapper.addEventListener('mouseleave', mouseUp);
rangeWrapper.addEventListener('touchend', mouseUp);

Now we can take a look at the updateValue function. This function is responsible for updating the component values and moving the slider in correspondence with the cursor position. We have commented exhaustively every part of it, for a better understanding:

// Function to update the slider value
function updateValue() {
    // Clear animations if are still running
    anime.remove([rangeValues, rangeSliderPaths[0], rangeSliderPaths[1]]);

    // Calculate the `input` value using the current `y`
    rangeValue = parseInt(currentY * max / rangeHeight);
    // Calculate `scale` value for numbers
    scale = (rangeValue - rangeMin) / (rangeMax - rangeMin) * scaleMax;
    // Update `input` value
    rangeInput.value = rangeValue;
    // Update numbers values
    rangeValueNumberTop.innerText = max - rangeValue;
    rangeValueNumberBottom.innerText = rangeValue;
    // Translate range values
    rangeValues.style.transform = 'translateY(' + (rangeHeight - currentY) + 'px)';
    // Apply corresponding `scale` to numbers
    rangeValueNumberTop.style.transform = 'scale(' + (1 - scale) + ')';
    rangeValueNumberBottom.style.transform = 'scale(' + (1 - (scaleMax - scale)) + ')';

    // Some math calculations
    if (Math.abs(mouseDy) < mouseDyLimit) {
        lastMouseDy = mouseDy;
    } else {
        lastMouseDy = mouseDy < 0 ? -mouseDyLimit : mouseDyLimit;
    }

    // Calculate the `newSliderY` value to build the slider `path`
    newSliderY = currentY + lastMouseDy / mouseDyFactor;
    if (newSliderY < rangeMinY || newSliderY > rangeMaxY) {
        newSliderY = newSliderY < rangeMinY ? rangeMinY : rangeMaxY;
    }

    // Build `path` string and update `path` elements
    newPath = buildPath(lastMouseDy, rangeHeight - newSliderY);
    rangeSliderPaths[0].setAttribute('d', newPath);
    rangeSliderPaths[1].setAttribute('d', newPath);
}

As we have seen, within the previous function there is a call to the buildPath function, which is an essential piece in our component. This function will let us build the path for the slider, given the following parameters:

  • dy: distance in the y axis that the mouse has been moved since the mousedown or touchstart event.
  • ty: distance in the y axis that the path must be translated.

It also uses the mouseX value to draw the curve to the cursor position on the x axis, and return the path in String format:

// Function to build the slider `path`, using the given `dy` and `ty` values
function buildPath(dy, ty) {
    return 'M 0 ' + ty + ' q ' + mouseX + ' ' + dy + ' 320 0 l 0 480 l -320 0 Z';
}

Finally, let’s see how to achieve the interesting elastic effect:

// Function to simulate the elastic behavior
function elasticRelease() {
    // Morph the paths to the opposite direction, to simulate a strong elasticity
    anime({
        targets: rangeSliderPaths,
        d: buildPath(-lastMouseDy * 1.3, rangeHeight - (currentY - lastMouseDy / mouseDyFactor)),
        duration: 150,
        easing: 'linear',
        complete: function () {
            // Morph the paths to the normal state, using the `elasticOut` easing function (default)
            anime({
                targets: rangeSliderPaths,
                d: buildPath(0, rangeHeight - currentY),
                duration: 4000,
                elasticity: 880
            });
        }
    });

    // Here will go a similar code to:
    // - Translate the values to the opposite direction, to simulate a strong elasticity
    // - Then, translate the values to the right position, using the `elasticOut` easing function (default)
}

As you can see, it was necessary to implement two consecutive animations to achieve an exaggerated elastic effect, similar to the original animation. This is because a single animation using the elasticOut easing function is not enough.

Conclusion

In this tutorial, we have developed a component to simulate the behavior of an input of type range, but with an impressive effect, similar to the original animation:

Dribble Shot by Stan Yakusevich

You can check the final result, play with the code on Codepen, or get the full code on Github.

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about our products

About the authors
Default avatar
Luis Manuel

author

While we believe that this content benefits our community, we have not yet thoroughly reviewed it. If you have any suggestions for improvements, please let us know by clicking the “report an issue“ button at the bottom of the tutorial.

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
Leave a comment


This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Featured on Community

Get our biweekly newsletter

Sign up for Infrastructure as a Newsletter.

Hollie's Hub for Good

Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.

Become a contributor

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

Welcome to the developer cloud

DigitalOcean makes it simple to launch in the cloud and scale up as you grow — whether you're running one virtual machine or ten thousand.

Learn more