纯CSS 做的 带有数字指示器的滑动进度条源码分析:附带每个关键步骤的代码和运行录屏

905 阅读20分钟

🍀 每日思考:选择进攻还是一味的防守。木偶人到底是趁风而行还是被风裹挟。你把飞翔看作危险还是勇气。来自知名UP彭春花:www.bilibili.com/video/BV1iw…idfrom=333.999.0.0&vd_source=b6b1d9d6f51f2ce7404dfce09c96dead

🍀 本文包括:项目介绍+项目链接+项目代码+项目展示+项目分解+分解展示+原理总结+兼容情况

🍀 本专栏致力于分析一些网上热门项目,如果本文对你有帮助的话,别忘了个点个赞嗷🌷。我是Wandra,我们下期再见✨

项目介绍

🍀 通过纯css,尤其是利用animation-timeline和css计数器来实现自定义的滑动进度条以及数字展示效果以及动画效果。

项目链接

项目展示

CleanShot 2024-09-07 at 20.52.49.gif

项目代码

🍀 html

<label>
  <input type="range" id="one" min="0" max="120" step="1" value="20">
  <output for="one" style="--min: 0;--max: 120"></output>
</label>  
<label style="--c: #BD1550">
  <input type="range" id="two" min="-50" max="50" step="1" value="0">
  <output class="bottom" for="two" style="--min: -50;--max: 50"></output>
</label>  

🍀 css

@property --val {
  syntax: '<integer>';
  inherits: true;
  initial-value: 0; 
}
@property --e {
  syntax: '<number>';
  inherits: true;
  initial-value: 0; 
}

label {
  --c: #547980; /* slider color */
  --g: round(.3em,1px);  /* the gap */
  --l: round(.2em,1px);  /* line thickness*/
  --s: round(1.3em,1px); /* thumb size*/
  --t: round(.8em,1px);  /* tooltip tail size */
  --r: round(.8em,1px);  /* tooltip radius */

  timeline-scope: --thumb-view;
  position: relative; /* No, It's not useless so don't remove it (or remove it and see what happens) */ 
  font-size: 24px;
}

input {
  width: 400px;
  height: var(--s); /* needed for Firefox*/
  --_c: color-mix(in srgb, var(--c), #000 var(--p,0%));
  appearance :none;
  background: none;
  cursor: pointer;
  overflow: hidden;
  font-size: inherit;
}
input:focus-visible,
input:hover{
  --p: 25%;
}
input:active,
input:focus-visible{
  --_b: var(--s)
}
/* chromium */
input[type="range" i]::-webkit-slider-thumb{
  height: var(--s);
  aspect-ratio: 1;
  border-radius: 50%;
  box-shadow: 0 0 0 var(--_b,var(--l)) inset var(--_c);
  border-image: linear-gradient(90deg,var(--_c) 50%,#ababab 0) 0 1/calc(50% - var(--l)/2) 100vw/0 calc(100vw + var(--g));
  -webkit-appearance: none;
  appearance: none;
  transition: .3s;
  anchor-name: --thumb;
  view-timeline: --thumb-view inline;
}
/* Firefox */
input[type="range"]::-moz-range-thumb {
  height: var(--s);
  width: var(--s);
  background: none;
  border-radius: 50%;
  box-shadow: 0 0 0 var(--_b,var(--l)) inset var(--_c);
  border-image: linear-gradient(90deg,var(--_c) 50%,#ababab 0) 0 1/calc(50% - var(--l)/2) 100vw/0 calc(100vw + var(--g));
  -moz-appearance: none;
  appearance: none;
  transition: .3s;
  anchor-name: --thumb;
  view-timeline: --thumb-view inline;
}
output {
  animation: range linear both;
  animation-timeline: --thumb-view;
  animation-range: entry 100% exit 0%;
}
output:before {
  content: counter(num);
  counter-reset: num var(--val);
  position-anchor: --thumb;
  position: absolute;
  inset-area: top;
  color: #fff;
  font-weight: bold;
  font-family: sans-serif;
  text-align: center;
  padding-block: .5em;
  width: 4em;
  background: inherit;
  --e: var(--val);
  transition: --e .1s ease-out;
  rotate: calc((var(--e) - var(--val))*2deg);
  transform-origin: 50% calc(100% + var(--s)/2);
  border-bottom: var(--t) solid #0000;
  border-radius: var(--r)/var(--r) var(--r) calc(var(--r) + var(--t)) calc(var(--r) + var(--t));
  --_m: 100%/var(--t) calc(var(--t) + 1px) no-repeat;
  --_g: 100%,#0000 99%,#000 102%;
  mask:
    linear-gradient(#000 0 0) padding-box,
    radial-gradient(100% 100% at 100% var(--_g)) calc(50% + var(--t)/2) var(--_m),
    radial-gradient(100% 100% at 0    var(--_g)) calc(50% - var(--t)/2) var(--_m);
}
output.bottom:before {
  inset-area: bottom;
  border-top: var(--t) solid #0000;
  border-bottom: none;
  rotate: calc((var(--val) - var(--e))*2deg);
  transform-origin: 50% calc(var(--s)/-2);
  border-radius: var(--r)/calc(var(--r) + var(--t)) calc(var(--r) + var(--t)) var(--r) var(--r);
  --_m: 0%/var(--t) calc(var(--t) + 1px) no-repeat;
  --_g: 0%,#0000 99%,#000 102%;
}

@keyframes range {
  0%   {background: #8A9B0F;--val:var(--max)}
  100% {background: #CC333F;--val:var(--min)}
}

@supports not (anchor-name: ---) {
  output {
    display: none;
  }
}

/**/
body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  gap: 20px;
  place-content: center;
  background: repeating-linear-gradient(-45deg, #fff 0 20px, #f9f9f9 0 40px);
}

项目分解

⚠️ 开篇须知:每个步骤部分都有该步骤的关键伪代码以及完整代码,可独立运行,并附带有该步骤运行的效果录屏,步骤依次走完之后,就能得到和原始项目相同的的项目成果,希望这篇文章能带你结合实际项目体会、学习、复习、扩大关于css的世界🗺️

🍀 本专栏致力于分析一些网上热门项目,如果本文对你有帮助的话,别忘了个点个赞嗷🌷。我是Wandra,我们下期再见✨

1️⃣ 关键步骤1:原生进度条+跟踪进度的带颜色动画的指示器

关键代码:伪代码(该关键步骤的完整代码在下面~~)

↖️ 使用type = "range"的input来做原生的滑动进度条, 用于应该包含一定范围内的值的输入字段,根据浏览器支持,输入字段能够显示为滑块控件,您能够使用如下属性来规定限制:min、max、step、value。

// 伪代码 
<input type="range" id="one" min="0" max="120" step="1" value="20">

↖️ 利用在input[type="range" i]::-webkit-slider-thumb 定义 "anchor-name: --thumb" 与定义了 "position-anchor: --thumb;"的output:before关联起来,使得output:before(指示器,将来会显示进度的数字)可以跟随webkit-slider-thumb(进度条的滑块)的滑动

  • 锚点定位(position-anchor)允许我们基于其它锚点元素的位置和尺寸去定位上下文,而不是传统意义上的基于父元素去进行绝对定位。
// 伪代码
 input[type="range" i]::-webkit-slider-thumb {
           // 通过新的锚点名称(anchor-name)来标记元素,允许我们使用这些经过了标记的元素作为我们绝对定位的基准目标;
            anchor-name: --thumb; 
        }
 output:before {
            content: "a";
            // 我们可以在绝对定位的元素上,通过新的语法 anchor() 或者 anchor-size() 来锚定上述被标记了的元素,并且可以使用被标记元素的相应属性(譬如被标记元素的 topleftrightbottom 等)
           // 还有一些更高级的用法,譬如锚点定位的 Fallback 机制,也就是可以设置多套不同的锚点定位规则,以适应更为复杂的页面布局情
            position-anchor: --thumb;
            position: absolute;
            inset-area: top;
            width: 4em;
        }

↖️ 指示器(output:before的父元素也就是output设置颜色不断变化的动画,该动画将来会采用视图驱动)

 @keyframes range {
            0% {
                background: #8A9B0F;
            }

            100% {
                background: #CC333F;
            }
        }

        output {
            animation: range 1s linear both;
        }

🍀 代码展示:该步骤的完整代码

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        body {
            margin: 0;
            min-height: 100vh;
            display: grid;
            gap: 20px;
            place-content: center;
            background: repeating-linear-gradient(-45deg, #fff 0 20px, #f9f9f9 0 40px);
        }

        input {
            width: 400px;
            cursor: pointer;
            font-size: inherit;

        }

        input[type="range" i]::-webkit-slider-thumb {
            anchor-name: --thumb;
        }

        @keyframes range {
            0% {
                background: #8A9B0F;
            }

            100% {
                background: #CC333F;
            }
        }

        output {
            animation: range 1s linear both;
        }

        output:before {
            content: "a";
            position-anchor: --thumb;
            position: absolute;
            inset-area: top;
            color: #fff;
            font-weight: bold;
            font-family: sans-serif;
            text-align: center;
            padding-block: .5em;
            width: 4em;
            background: inherit;
        }
    </style>
</head>


<body>
    <label>
        <input type="range" id="one" min="0" max="120" step="1" value="20"><output for="one"
            style="--min: 0;--max: 120"></output></label>
</body>

</html>

🍀 效果展示

CleanShot 2024-09-17 at 22.53.58.gif

2️⃣ 关键步骤2:给指示器添加表示当前在滑动条中滑块滑动时所处的进度的数字

🍀 关键代码:伪代码(该关键步骤的完整代码在下面~~)

↖️ counter-reset 在指定元素上创建给定名字的计数器, 可以给定初始计数器的值, 如果没有给, 默认为 0。初始值只能是整数类型(包括负数). 小数或百分数都不行。counter(): 展示计数器的值. 通常搭配在伪类元素中搭配 content 属性使用.

        output:before {
            content: counter(num);
            counter-reset: num var(--val);
        }


        output {
            animation: range 1s linear both;
        }


        @keyframes range {
            0% {
                --val: var(--max)
            }

            100% {
                --val: var(--min)
            }
        }

         <output for="one" style="--min: 0;--max: 120"></output>

↖️ 与使用:root定义自定义属性不同,使用:root定义的自定义属性如果不支持动画过渡(比如计数器会直接从0跳到120,从min跳到max值),但是@property为自定义属性实现动画过渡提供了可能,采用@property为实现计数器从min - max的动画过渡(从0一个一个值的跳到120)打下了基础。

// 这个@property --val必须加,而且必须是整数 interger,不能是number类型,否则数字动画不会从120到0挨个挨个的过渡,会直接从120切换到0
        @property --val {
            syntax: '<integer>';
            inherits: true;
            initial-value: 0;
        }

🍀 完整代码展示:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        body {
            margin: 0;
            min-height: 100vh;
            display: grid;
            gap: 20px;
            place-content: center;
            background: repeating-linear-gradient(-45deg, #fff 0 20px, #f9f9f9 0 40px);
        }



        label {
            --s: round(1.3em, 1px);
            --l: round(.2em, 1px);
            --g: round(.3em, 1px);
            position: relative;
        }

        input {
            width: 400px;
            cursor: pointer;
            font-size: inherit;
        }

        input[type="range" i]::-webkit-slider-thumb {
            anchor-name: --thumb;
        }

        @keyframes range {
            0% {
                background: #8A9B0F;
                --val: var(--max)
            }

            100% {
                background: #CC333F;
                --val: var(--min)
            }
        }


        output {
            animation: range 1s linear both;
        }

        // 这个@property --val必须加,否则数字动画不会从120到0挨个挨个的过渡,会直接从120切换到0
        @property --val {
            syntax: '<integer>';
            inherits: true;
            initial-value: 0;
        }

        output:before {
            content: counter(num);
            counter-reset: num var(--val);
            position-anchor: --thumb;
            position: absolute;
            inset-area: top;
            color: #fff;
            font-weight: bold;
            font-family: sans-serif;
            text-align: center;
            padding-block: .5em;
            width: 4em;
            background: inherit;
        }
    </style>
</head>

<body>
    <label>
        <input type="range" id="one" min="0" max="120" step="1" value="20">
        <output for="one" style="--min: 0;--max: 120"></output>
    </label>

</body>

</html>

🍀 效果展示:

CleanShot 2024-09-18 at 21.42.58.gif

3️⃣ 关键步骤3:将指示器的基于时间的背景颜色的动画以及数字从max过度到min的动画修成给基于视图驱动的动画

🍀 关键代码:伪代码(该关键步骤的完整代码在下面~~)

↖️ 基于时间的背景颜色的动画以及数字从max过度到min的动画不再由动画时间驱动而自动执行,而是由要执行动画的元素(数字指示器)和该元素(本项目中是指的是该元素的兄弟元素,即滑块元素)所在视图容器的视图交叉情况来驱动。

↖️ 想要实现兄弟元素(input滑块)驱动兄弟元素(数字指示器)。,在需要执行动画的数字指示器元素上使用 animation-timeline 表示需要一个具名时间线来驱动动画,在 滑块 元素上使用 view-timeline: --thumb-view inline; 表示由 滑块 元素来提供时间线.最后, 在 滑块 元素 和 数字指示器元素 的共同祖先 label元素 上使用 timeline-scope:--thumb-view 表示时间线范围提升到 label元素 和 label元素 的任何后代元素上。

↖️ 具体可以参考 juejin.cn/post/729194…

        label {
            timeline-scope: --thumb-view;
        }

        input[type="range" i]::-webkit-slider-thumb {
            view-timeline: --thumb-view inline;
        }

        output {
            animation: range 1s linear both;
            animation-timeline: --thumb-view;
            animation-range: entry 100% exit 0%;
        }


        @keyframes range {
            0% {
                background: #8A9B0F;
                --val: var(--max)
            }

            100% {
                background: #CC333F;
                --val: var(--min)
            }
        }

🍀 完整代码展示:


<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        body {
            margin: 0;
            min-height: 100vh;
            display: grid;
            gap: 20px;
            place-content: center;
            background: repeating-linear-gradient(-45deg, #fff 0 20px, #f9f9f9 0 40px);
        }


        label {
            --s: round(1.3em, 1px);
            --l: round(.2em, 1px);
            --g: round(.3em, 1px);
            timeline-scope: --thumb-view;
            position: relative;
        }

        input {
            width: 400px;
            cursor: pointer;
            font-size: inherit;
        }

        input[type="range" i]::-webkit-slider-thumb {
            anchor-name: --thumb;
            view-timeline: --thumb-view inline;
        }


        @keyframes range {
            0% {
                background: #8A9B0F;
                --val: var(--max)
            }

            100% {
                background: #CC333F;
                --val: var(--min)
            }
        }

        output {
            animation: range 1s linear both;
            animation-timeline: --thumb-view;
            animation-range: entry 100% exit 0%;
        }


        @property --val {
            syntax: '<integer>';
            inherits: true;
            initial-value: 0;
        }

        output:before {
            content: counter(num);
            counter-reset: num var(--val);
            position-anchor: --thumb;
            position: absolute;
            inset-area: top;
            color: #fff;
            font-weight: bold;
            font-family: sans-serif;
            text-align: center;
            padding-block: .5em;
            width: 4em;
            background: inherit;
        }
    </style>
</head>

<body>
    <label>
        <input type="range" id="one" min="0" max="120" step="1" value="20">
        <output for="one" style="--min: 0;--max: 120"></output>
    </label>

</body>

</html>

🍀 效果展示:

CleanShot 2024-09-18 at 21.55.55.gif

4️⃣ 关键步骤4:给指示器添加基于视图驱动的rotate动画

🍀 关键代码:伪代码(该关键步骤的完整代码在下面~~)

↖️ 当滑动条的值改变时,--val 这个 CSS 自定义属性的值也随之改变。output::before 中的 rotate 样式依赖于 --e--val 两个变量之间的差异。rotate 样式使用了 calc() 函数来计算旋转的角度,具体为 (var(--e) - var(--val)) * 2deg。这里 --e 是一个表示旧值的变量,而 --val 是新值。每当值改变时,--e 会被设置成 --val 的旧值,然后逐渐过渡到新值。transition 属性 --e .1s ease-out 控制着这个过渡的速度。

↖️ 在这个例子中,--e--val 的初始值是相同的,但是在滑动过程中它们的值是不同的,这是因为它们代表的是不同的概念:--val 是当前的值,即滑块当前所处的位置对应的值,--e 则是一个用来过渡的属性,它的初始值被设置为 --val 的值,但在每次 --val 改变之后,--e 会通过 transition 动画逐渐过渡到新的 --val 值。因此,在过渡期间,--e--val 是不同的,这就导致了 rotate 计算结果的变化。

↖️ 当用户快速移动滑块时,--val 的变化速度较快,因此 (var(--e) - var(--val)) 的差值较大,导致 rotate 的角度较大;相反,如果用户缓慢地移动滑块,则 --val 的变化较慢,因此 (var(--e) - var(--val)) 的差值较小,从而导致 rotate 的角度较小。

↖️ 例如,如果 --val 从 20 变为 60,那么 --e 将从 20 开始逐渐过渡到 60,而这个过渡是通过 .1s ease-out 完成的。如果滑动得很快,--val 可能会在 --e 完全过渡到新值之前再次改变,这样 (var(--e) - var(--val)) 就会是一个较大的数,从而导致更大的旋转角度。

        output:before {
            --e: var(--val);
            transition: --e .1s ease-out;
            rotate: calc((var(--e) - var(--val))*2deg);
            transform-origin: 50% calc(100% + var(--s)/2);
        }

         @property --e {
            syntax: '<number>';
            inherits: true;
            initial-value: 0;
        }

🍀 完整代码展示:


<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        body {
            margin: 0;
            min-height: 100vh;
            display: grid;
            gap: 20px;
            place-content: center;
            background: repeating-linear-gradient(-45deg, #fff 0 20px, #f9f9f9 0 40px);
        }

        label {
            --s: round(1.3em, 1px);
            --l: round(.2em, 1px);
            --g: round(.3em, 1px);
            timeline-scope: --thumb-view;
            position: relative;
        }

        input {
            width: 400px;
            cursor: pointer;
            font-size: inherit;
        }

        input[type="range" i]::-webkit-slider-thumb {
            anchor-name: --thumb;
            view-timeline: --thumb-view inline;
        }



        @keyframes range {
            0% {
                background: #8A9B0F;
                --val: var(--max)
            }

            100% {
                background: #CC333F;
                --val: var(--min)
            }
        }

        output {
            animation: range 1s linear both;
            animation-timeline: --thumb-view;
            animation-range: entry 100% exit 0%;
        }

        @property --val {
            syntax: '<integer>';
            inherits: true;
            initial-value: 0;
        }

        @property --e {
            syntax: '<number>';
            inherits: true;
            initial-value: 0;
        }

        output:before {
            content: counter(num);
            counter-reset: num var(--val);
            position-anchor: --thumb;
            position: absolute;
            inset-area: top;
            color: #fff;
            font-weight: bold;
            font-family: sans-serif;
            text-align: center;
            padding-block: .5em;
            width: 4em;
            background: inherit;
            --e: var(--val);
            transition: --e .1s ease-out;
            rotate: calc((var(--e) - var(--val))*2deg);
            transform-origin: 50% calc(100% + var(--s)/2);
        }
    </style>
</head>

<body>
    <label>
        <input type="range" id="one" min="0" max="120" step="1" value="20">
        <output for="one" style="--min: 0;--max: 120"></output>
    </label>

</body>

</html>

🍀 效果展示:

CleanShot 2024-09-18 at 22.28.51.gif

5️⃣ 关键步骤5:自定义滑动条和滑块的样式

🍀 关键代码:伪代码:(该关键步骤的完整代码在下面~~)

↖️ appearance: none;: 移除默认的外观样式,以便我们可以完全自定义滑动条的外观。overflow: hidden;: 隐藏超出滑动条的border-image。--_c: color-mix(in srgb, var(--c), #000 var(--p, 0%));: 使用 color-mix 函数混合颜色,这里的 --c--p 是可自定义的颜色和百分比,用于调整滑块的颜色。aspect-ratio: 1;: 设置滑块为正方形。box-shadow: 0 0 0 var(--*b, var(--l)) inset var(--*c);给滑块添加内阴影,阴影的大小由 --*b 或 --l 决定,颜色为 --*c。

↖️ 使用border-image: linear-gradient(90deg, var(--*c) 50%, #ababab 0) 0 1/calc(50% - var(--l)/2) 100vw/0 calc(100vw + var(--g))模拟滑动条,创建了一个水平方向的线性渐变,从滑块的位置开始,左边颜色是--*c,右边颜色是#ababab的灰色。

  • 0 1: 指定边框图像应该被裁剪的部分。这里 0 表示边框图像的四个角落都不裁剪,1 表示在水平方向上,边框图像被裁剪为1单位长度,这意味着只有水平方向上的图像被使用,垂直方向上的图像被忽略。
  • calc(50% - var(--l)/2): 计算出一个宽度值,它等于50%减去 var(--l) 的一半。这通常是为了让边框看起来居中或保持特定的比例。
  • 100vw: 使用视口宽度(viewport width)作为水平方向上的宽度。这通常是为了让边框充满整个可见区域的宽度。
  • 0: 垂直方向上没有填充,因为前面提到过垂直方向上的边框图像被裁剪掉了。
  • calc(100vw + var(--g)): 水平方向上的填充,100vw 加上 var(--g) 的值。这使得水平方向上的边框图像可以延伸超过视口的宽度,var(--g) 可以用于增加额外的宽度。
        input {
            width: 400px;
            height: var(--s);
            cursor: pointer;
            font-size: inherit;
            --_c: color-mix(in srgb, var(--c), #000 var(--p, 0%));
            appearance: none;
            overflow: hidden;
            font-size: inherit;

        }

    input[type="range" i]::-webkit-slider-thumb {
            height: var(--s);
            aspect-ratio: 1;
            border-radius: 50%;
            box-shadow: 0 0 0 var(--_b, var(--l)) inset var(--_c);
            border-image: linear-gradient(90deg, var(--_c) 50%, #ababab 0) 0 1/calc(50% - var(--l)/2) 100vw/0 calc(100vw + var(--g));
            -webkit-appearance: none;
            appearance: none;
        }

🍀 完整代码展示: 🍀 完整代码展示:


<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        body {
            margin: 0;
            min-height: 100vh;
            display: grid;
            gap: 20px;
            place-content: center;
            background: repeating-linear-gradient(-45deg, #fff 0 20px, #f9f9f9 0 40px);
        }

        label {
            --s: round(1.3em, 1px);
            --l: round(.2em, 1px);
            --g: round(.3em, 1px);
            timeline-scope: --thumb-view;
            position: relative;
            --c: #547980;
        }

        input {
            width: 400px;
            height: var(--s);
            cursor: pointer;
            font-size: inherit;
            --_c: color-mix(in srgb, var(--c), #000 var(--p, 0%));
            appearance: none;
            background: none;
            overflow: hidden;
            font-size: inherit;

        }

        input[type="range" i]::-webkit-slider-thumb {
            anchor-name: --thumb;
            height: var(--s);
            aspect-ratio: 1;
            border-radius: 50%;
            box-shadow: 0 0 0 var(--_b, var(--l)) inset var(--_c);
            border-image: linear-gradient(90deg, var(--_c) 50%, #ababab 0) 0 1/calc(50% - var(--l)/2) 100vw/0 calc(100vw + var(--g));
            -webkit-appearance: none;
            appearance: none;
            transition: .3s;
            view-timeline: --thumb-view inline;
        }


        @keyframes range {
            0% {
                background: #8A9B0F;
                --val: var(--max)
            }

            100% {
                background: #CC333F;
                --val: var(--min)
            }
        }


        output {
            animation: range 1s linear both;
            animation-timeline: --thumb-view;
            animation-range: entry 100% exit 0%;
        }

        @property --val {
            syntax: '<integer>';
            inherits: true;
            initial-value: 0;
        }

        @property --e {
            syntax: '<number>';
            inherits: true;
            initial-value: 0;
        }

        output:before {
            content: counter(num);
            counter-reset: num var(--val);
            position-anchor: --thumb;
            position: absolute;
            inset-area: top;
            color: #fff;
            font-weight: bold;
            font-family: sans-serif;
            text-align: center;
            padding-block: .5em;
            width: 4em;
            background: inherit;
            --e: var(--val);
            transition: --e .1s ease-out;
            rotate: calc((var(--e) - var(--val))*2deg);
            transform-origin: 50% calc(100% + var(--s)/2);
        }
    </style>
</head>

<body>
    <label>
        <input type="range" id="one" min="0" max="120" step="1" value="20">
        <output for="one" style="--min: 0;--max: 120"></output>
    </label>

</body>

</html>

🍀 效果展示:

CleanShot 2024-09-18 at 23.02.46.gif

6️⃣ 关键步骤6:设计优化数字指示器的形状

🍀 关键代码:伪代码:这里主要是靠border-radius和mask实现该形状效果,鉴于用法比较复杂,具体用法可自行google

        label {
            --t: round(.8em, 1px);
            --r: round(.8em, 1px);
        }


        output:before {
            border-bottom: var(--t) solid #0000;
            border-radius: var(--r)/var(--r) var(--r) calc(var(--r) + var(--t)) calc(var(--r) + var(--t));
            --_m: 100%/var(--t) calc(var(--t) + 1px) no-repeat;
            --_g: 100%, #0000 99%, #000 102%;
            mask:
                linear-gradient(#000 0 0) padding-box,
                radial-gradient(100% 100% at 100% var(--_g)) calc(50% + var(--t)/2) var(--_m),
                radial-gradient(100% 100% at 0 var(--_g)) calc(50% - var(--t)/2) var(--_m);
        }

🍀 完整代码展示:


<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        body {
            margin: 0;
            min-height: 100vh;
            display: grid;
            gap: 20px;
            place-content: center;
            background: repeating-linear-gradient(-45deg, #fff 0 20px, #f9f9f9 0 40px);
        }


        label {
            --s: round(1.3em, 1px);
            --l: round(.2em, 1px);
            --g: round(.3em, 1px);
            timeline-scope: --thumb-view;
            position: relative;
            --c: #547980;
            --t: round(.8em, 1px);
            --r: round(.8em, 1px);
            /* font-size: 24px; */
        }

        input {
            width: 400px;
            height: var(--s);
            cursor: pointer;
            font-size: inherit;



            --_c: color-mix(in srgb, var(--c), #000 var(--p, 0%));
            appearance: none;
            background: none;
            overflow: hidden;
            font-size: inherit;

        }

        input[type="range" i]::-webkit-slider-thumb {
            anchor-name: --thumb;
            height: var(--s);
            aspect-ratio: 1;
            border-radius: 50%;
            box-shadow: 0 0 0 var(--_b, var(--l)) inset var(--_c);
            border-image: linear-gradient(90deg, var(--_c) 50%, #ababab 0) 0 1/calc(50% - var(--l)/2) 100vw/0 calc(100vw + var(--g));
            -webkit-appearance: none;
            appearance: none;
            transition: .3s;
            view-timeline: --thumb-view inline;
        }

        @keyframes range {
            0% {
                background: #8A9B0F;
                --val: var(--max)
            }

            100% {
                background: #CC333F;
                --val: var(--min)
            }
        }

        output {
            animation: range 1s linear both;
            animation-timeline: --thumb-view;
            animation-range: entry 100% exit 0%;
        }

        @property --val {
            syntax: '<integer>';
            inherits: true;
            initial-value: 0;
        }

        @property --e {
            syntax: '<number>';
            inherits: true;
            initial-value: 0;
        }

        output:before {
            content: counter(num);
            counter-reset: num var(--val);
            position-anchor: --thumb;
            position: absolute;
            inset-area: top;
            color: #fff;
            font-weight: bold;
            font-family: sans-serif;
            text-align: center;
            padding-block: .5em;
            width: 4em;
            background: inherit;
            --e: var(--val);
            transition: --e .1s ease-out;
            rotate: calc((var(--e) - var(--val))*2deg);
            transform-origin: 50% calc(100% + var(--s)/2);
            border-bottom: var(--t) solid #0000;
            border-radius: var(--r)/var(--r) var(--r) calc(var(--r) + var(--t)) calc(var(--r) + var(--t));
            --_m: 100%/var(--t) calc(var(--t) + 1px) no-repeat;
            --_g: 100%, #0000 99%, #000 102%;
            mask:
                linear-gradient(#000 0 0) padding-box,
                radial-gradient(100% 100% at 100% var(--_g)) calc(50% + var(--t)/2) var(--_m),
                radial-gradient(100% 100% at 0 var(--_g)) calc(50% - var(--t)/2) var(--_m);
        }
    </style>
</head>

<body>
    <label>
        <input type="range" id="one" min="0" max="120" step="1" value="20">
        <output for="one" style="--min: 0;--max: 120"></output>
    </label>

</body>

</html>

🍀 效果展示:

CleanShot 2024-09-18 at 23.46.57.gif

7️⃣ 关键步骤7:优化滑块点击效果/优化锚定定位兼容效果/增加第二个进度条

🍀 关键代码:伪代码:

↖️ 点击滑块会修改box-shadow阴影的宽度

↖️ 如果浏览器不支持anchor-name,也就是锚点定位,那么久不现实数字指示器

↖️ 依据以上步骤实现的滑动条再采用类似思想实现另一个指示器位置位于进度条底部的进度条


    input:active,
    input:focus-visible {
        --_b: var(--s)
    }

    @supports not (anchor-name: ---) {
        output {
            display: none;
        }
    }

     output.bottom:before {
        inset-area: bottom;
        border-top: var(--t) solid #0000;
        border-bottom: none;
        rotate: calc((var(--val) - var(--e))*2deg);
        transform-origin: 50% calc(var(--s)/-2);
        border-radius: var(--r)/calc(var(--r) + var(--t)) calc(var(--r) + var(--t)) var(--r) var(--r);
        --_m: 0%/var(--t) calc(var(--t) + 1px) no-repeat;
        --_g: 0%, #0000 99%, #000 102%;
    }

<label style="--c: #BD1550">
    <input type="range" id="two" min="-50" max="50" step="1" value="0">
    <output class="bottom" for="two" style="--min: -50;--max: 50"></output>
</label>

🍀 完整代码展示


<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        @property --val {
            syntax: '<integer>';
            inherits: true;
            initial-value: 0;
        }

        @property --e {
            syntax: '<number>';
            inherits: true;
            initial-value: 0;
        }

        label {
            --s: round(1.3em, 1px);
            --l: round(.2em, 1px);
            --g: round(.3em, 1px);
            timeline-scope: --thumb-view;
            position: relative;




            --c: #547980;
            --t: round(.8em, 1px);
            --r: round(.8em, 1px);
            /* font-size: 24px; */
        }

        input {
            width: 400px;
            height: var(--s);
            cursor: pointer;
            font-size: inherit;
            --_c: color-mix(in srgb, var(--c), #000 var(--p, 0%));
            appearance: none;
            background: none;
            overflow: hidden;
            font-size: inherit;
        }

        input:focus-visible,
        input:hover {
            --p: 25%;
        }

        input:active,
        input:focus-visible {
            --_b: var(--s)
        }

        input[type="range" i]::-webkit-slider-thumb {
            anchor-name: --thumb;
            height: var(--s);
            aspect-ratio: 1;
            border-radius: 50%;
            box-shadow: 0 0 0 var(--_b, var(--l)) inset var(--_c);
            border-image: linear-gradient(90deg, var(--_c) 50%, #ababab 0) 0 1/calc(50% - var(--l)/2) 100vw/0 calc(100vw + var(--g));
            -webkit-appearance: none;
            appearance: none;
            transition: .3s;
            view-timeline: --thumb-view inline;
        }



        output {
            animation: range 1s linear both;
            animation-timeline: --thumb-view;
            animation-range: entry 100% exit 0%;
        }

        output:before {
            content: counter(num);
            counter-reset: num var(--val);
            position-anchor: --thumb;
            position: absolute;
            inset-area: top;
            color: #fff;
            font-weight: bold;
            font-family: sans-serif;
            text-align: center;
            padding-block: .5em;
            width: 4em;
            background: inherit;
            --e: var(--val);
            transition: --e .1s ease-out;
            rotate: calc((var(--e) - var(--val))*2deg);
            transform-origin: 50% calc(100% + var(--s)/2);
            border-bottom: var(--t) solid #0000;
            border-radius: var(--r)/var(--r) var(--r) calc(var(--r) + var(--t)) calc(var(--r) + var(--t));
            --_m: 100%/var(--t) calc(var(--t) + 1px) no-repeat;
            --_g: 100%, #0000 99%, #000 102%;
            mask:
                linear-gradient(#000 0 0) padding-box,
                radial-gradient(100% 100% at 100% var(--_g)) calc(50% + var(--t)/2) var(--_m),
                radial-gradient(100% 100% at 0 var(--_g)) calc(50% - var(--t)/2) var(--_m);
        }

        output.bottom:before {
            inset-area: bottom;
            border-top: var(--t) solid #0000;
            border-bottom: none;
            rotate: calc((var(--val) - var(--e))*2deg);
            transform-origin: 50% calc(var(--s)/-2);
            border-radius: var(--r)/calc(var(--r) + var(--t)) calc(var(--r) + var(--t)) var(--r) var(--r);
            --_m: 0%/var(--t) calc(var(--t) + 1px) no-repeat;
            --_g: 0%, #0000 99%, #000 102%;
        }

        @keyframes range {
            0% {
                background: #8A9B0F;
                --val: var(--max)
            }

            100% {
                background: #CC333F;
                --val: var(--min)
            }
        }

        @supports not (anchor-name: ---) {
            output {
                display: none;
            }
        }

        body {
            margin: 0;
            min-height: 100vh;
            display: grid;
            gap: 20px;
            place-content: center;
            background: repeating-linear-gradient(-45deg, #fff 0 20px, #f9f9f9 0 40px);
        }
    </style>
</head>

<body>
    <label>
        <input type="range" id="one" min="0" max="120" step="1" value="20">
        <output for="one" style="--min: 0;--max: 120"></output>
    </label>
    <label style="--c: #BD1550">
        <input type="range" id="two" min="-50" max="50" step="1" value="0">
        <output class="bottom" for="two" style="--min: -50;--max: 50"></output>
    </label>
</body>

</html>

🍀 效果展示

CleanShot 2024-09-18 at 23.54.00.gif

原理总结

🍀 本质上:依靠视图驱动、css 计数器、自定义滑动条等实现该效果。

兼容情况

🍀 这里只列出针对于项目使用的关键技术点~~

appearance

image.png

animation-timeline

image.png

view-timeline

image.png

animation-range

image.png

border-image

image.png

position-anchor

image.png

counter-reset

image.png

::-webkit-slider-thumb

image.png

参考资源

🍀 本专栏致力于分析一些网上热门项目,如果本文对你有帮助的话,别忘了个点个赞嗷🌷。我是Wandra,我们下期再见✨