在我们可以在任何UI中找到的所有迷人的web组件中,表单通常是最无聊的部分。在过去,只有一些文本输入元素,用户必须手动输入数据。然后随着HTML5的出现,事情有了很大的改进,因为出现了新的input
类型,比如color
、date
和range
等等。
虽然这些新型的input
在功能上是可行的,但是我们可以说它们往往不能满足web应用程序的审美需求,于是出现了许多建议来替换这些元素,以获得更好的外观,并且在所有现代浏览器中看起来几乎是一样的。
在本教程中,我们将了解如何使用以下优雅的组件模拟range
input
的行为:
表的内容

这个原创动画,我们用它作为灵感,可以在Stan Yakusevich的this dribble shot中找到。
为了对其进行编码,我们将主要使用SVG来绘制path
和anime.js来执行动画。
以上是我们的最终产品。让我们开始!
编写标记:HTML和SVG
接下来我们将看到我们将使用的主要HTML结构。请阅读评论,这样你就不会错过任何一个细节:
1 2 3 4 5 6 7 8 | <!-- Wrapper for the range input slider --> <div class="range__wrapper"> <!-- The real input, it 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> |
正如我们所看到的,我们的组件包含一个类型range
的实际 input
,我们将用Javascript正确地更新它。将这个input
元素和我们的组件放入一个通用HTML表单中,允许我们在input
时将输入值(连同其他表单数据)submit
到服务器。
现在让我们来看看需要的SVG元素,为了更好地理解,我们进行了注释:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | <!-- 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> |
如果这是您第一次使用SVG
path
元素,或者您不了解它们是如何工作的,那么您可以在MDN的这个优秀教程中了解更多。
最后,我们需要另一段代码来显示原始动画中出现的值和文本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <!-- 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> |
如您所见,如果遵循注释,HTML代码非常容易理解。现在我们来看看这些样式。
添加样式
我们将开始对wrapper
元素进行样式化
1 2 3 4 5 | .range__wrapper { user-select: none; // disable user selection, for better drag & drop // More code for basic styling and centering... } |
正如您所看到的,除了实现正确外观和以元素为中心的基本样式之外,我们还禁用了用户在组件中选择任何内容的能力。这一点非常重要,因为我们将实现“拖放”类型的交互,因此如果我们允许“选择”功能,就可以获得意外的行为。
接下来我们将隐藏实际的input
元素,并正确定位svg
(.range__slider
)元素:
1 2 3 4 5 6 7 8 9 10 11 | // Hide the `input` .range__input { display: none; } // Position the SVG root element .range__slider { position: absolute; left: 0; top: 0; } |
为了给SVG元素上色,我们使用以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // 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; } |
现在我们来看看这些值的主要样式。在这里,transform-origin在保持数字与文本按照期望的方式对齐方面扮演了重要的角色,就像在原始动画中一样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // 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... |
添加与Javascript的交互
现在是时候添加互动了,开始制作动画并享受其中的乐趣了:)
首先,让我们看看模拟拖放功能、侦听相应事件、执行数学工作和执行动画所需的代码。请注意,我们没有包含全部代码,只包含理解行为的基本部分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | // 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); |
现在我们可以看一下updateValue
函数。这个函数负责更新组件的值,并根据光标的位置移动滑块。为了更好地理解,我们详尽地评论了它的每一部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | // Function to update the slider value function updateValue() { // Clear animations if are still running anime.remove([rangeValues, rangeSliderPaths[0], rangeSliderPaths[1]]); // Calc the `input` value using the current `y` rangeValue = parseInt(currentY * max / rangeHeight); // Calc `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 maths calc if (Math.abs(mouseDy) < mouseDyLimit) { lastMouseDy = mouseDy; } else { lastMouseDy = mouseDy < 0 ? -mouseDyLimit : mouseDyLimit; } // Calc 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); } |
正如我们已经看到的,在前面的函数中有一个对buildPath
函数的调用,这是组件中必不可少的一部分。这个函数将让我们为滑块建立path
,给定以下参数:
dy
: 自mousedown
或touchstart
事件以来,鼠标在y
轴上移动的距离。ty
:y
轴上path
必须平移的距离。
它还使用mouseX
值将曲线绘制到光标在x轴上的位置,并以String
格式返回路径:
1 2 3 4 | // 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'; } |
最后,我们来看看如何实现有趣的弹性效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | // 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) } |
如您所见,需要实现两个连续的动画,以达到夸张的弹性效果,类似于原始动画。这是因为使用elasticOut函数的单个动画是不够的。
计算上升的总和
And finally we are done!
我们开发了一个组件来模拟input
类型range
的行为,但效果令人印象深刻,类似于原始动画:

您可以检查最终结果,在code on Codepen上使用代码,或者在Github上获得完整的代码。
请注意,为了使本教程更有趣、更容易理解,我们没有在这里解释所使用的每一行代码。但是,您可以在Github repository中找到完整的代码。
我们衷心希望您喜欢本教程,并从中得到启发!
原文:https://scotch.io/tutorials/build-an-elastic-range-input-with-svg-and-animejs