news 2026/10/31 2:16:12

【每天一个CSS | Day40】track、thumb、填充全手绘的自定义滑块

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【每天一个CSS | Day40】track、thumb、填充全手绘的自定义滑块

写在前面

昨天做了展开式搜索框,label包裹input、:focus-within开合、checkbox锁定。

今天的组件,是表单里最容易被忽略、也最难美化的一件:滑块。

浏览器默认的range输入框,是组件惊艳化里最扎眼的出厂件。

灰色轨道、灰色小圆钮、拖动时没有任何反馈,放在任何一个设计过的界面里都像是一个bug。

今天把它拆到最底层:轨道、拇指、填充段,三层全部手绘。webkit和Firefox两套伪元素体系一次讲全。

填充比例要实时跟随拖动,这需要一行JS。除此之外,全部是CSS。

这一篇也是本专栏第一次做“原生控件换皮”。

原生控件有自己的默认样式、自己的伪元素体系、自己的兼容性陷阱。做对了,比从头造一个更稳。


一、先看效果

深色声音设置卡片。

两根滑块纵向排列,间距够宽。

第一根:主音量,翡翠绿。左侧一个喇叭图标,右边数值60%。轨道6px圆角,已填充段是翡翠绿,未填充段是深灰,滑块是一枚白色圆钮带外圈。轨道下方三个刻度:0、50、100。

第二根:低音增强,天蓝。同结构,数值35%。

拖动时:白钮跟着手走,左侧填充段同步变长变短,右侧数值实时更新。按下去的时候,白钮外圈从3px扩散到6px,观感像真的按下去了一点。

Chrome和Edge走webkit体系,Firefox走-moz原生进度填充。两边都做了,观感一致。

怎么跑起来:

  1. 新建一个demo.html
  2. 把下面第二节的完整代码粘进去
  3. 双击用浏览器打开
  4. 拖到一半松手试试

截图党提示:建议做Before / After对比图。左半放浏览器默认滑块截图,右半放手绘滑块拖动GIF。反差即点击理由。


二、完整代码(复制即用)

<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>Day 40 · 自定义滑块</title><style>*{margin:0;padding:0;box-sizing:border-box;}html, body{height:100%;}body{display:flex;align-items:center;justify-content:center;background:#10151c;font-family:"PingFang SC",sans-serif;color:#e6edf3;padding:20px;}.card{width:420px;background:#161d26;border-radius:18px;padding:28px;border:1px solidrgba(255,255,255,.08);box-shadow:0 26px 60pxrgba(0,0,0,.45);}.t{font:400 10px/1"SF Mono",Consolas,monospace;letter-spacing:.3em;color:#5b6878;}h2{margin-top:12px;font-size:17px;font-weight:600;letter-spacing:.04em;}.row{margin-top:26px;}.row .head{display:flex;align-items:center;gap:9px;margin-bottom:14px;font-size:13.5px;color:#c7d1de;}.row .head .val{margin-left:auto;font:500 13px/1"SF Mono",Consolas,monospace;color:var(--fill);}.row svg{flex:none;}/* —— 滑块:webkit / moz 双体系 —— */input[type="range"]{-webkit-appearance:none;appearance:none;width:100%;height:20px;background:transparent;cursor:pointer;--v:60%;}input[type="range"].r2{--v:35%;}input[type="range"]::-webkit-slider-runnable-track{height:6px;border-radius:3px;background:linear-gradient(90deg,var(--fill)var(--v),#26303dvar(--v));}input[type="range"]::-webkit-slider-thumb{-webkit-appearance:none;width:20px;height:20px;border-radius:50%;background:#fff;margin-top:-7px;/* (track 6 - thumb 20) / 2 = -7 */box-shadow:0 2px 8pxrgba(0,0,0,.45),0 0 0 3pxrgba(255,255,255,.08);}input[type="range"]:active::-webkit-slider-thumb{box-shadow:0 2px 8pxrgba(0,0,0,.45),0 0 0 6pxrgba(92,201,180,.18);}input[type="range"]::-moz-range-track{height:6px;border-radius:3px;background:#26303d;}input[type="range"]::-moz-range-progress{height:6px;border-radius:3px;background:var(--fill);}input[type="range"]::-moz-range-thumb{width:20px;height:20px;border:0;border-radius:50%;background:#fff;box-shadow:0 2px 8pxrgba(0,0,0,.45);}.r1{--fill:#5cc9b4;}.r2{--fill:#7ab8ff;}.scale{display:flex;justify-content:space-between;margin-top:8px;font:400 9px/1"SF Mono",Consolas,monospace;letter-spacing:.14em;color:#39434f;}.hint{margin-top:24px;font:400 10px/1"SF Mono",Consolas,monospace;letter-spacing:.24em;color:#39434f;}.tag{position:fixed;right:18px;bottom:14px;font:400 10px/1"SF Mono",Consolas,monospace;letter-spacing:.28em;color:rgba(230,237,243,.22);}</style></head><body><divclass="card"><divclass="t">RANGE · T01</div><h2>声音设置</h2><divclass="row"><divclass="head"><svgwidth="15"height="15"viewBox="0 0 24 24"fill="none"stroke="#5cc9b4"stroke-width="2"stroke-linecap="round"stroke-linejoin="round"><pathd="M11 5 6 9H3v6h3l5 4z"/><pathd="M15.5 8.5a5 5 0 0 1 0 7"/></svg>主音量<spanclass="val">60%</span></div><inputclass="r1"type="range"min="0"max="100"value="60"><divclass="scale"><span>0</span><span>50</span><span>100</span></div></div><divclass="row"><divclass="head"><svgwidth="15"height="15"viewBox="0 0 24 24"fill="none"stroke="#7ab8ff"stroke-width="2"stroke-linecap="round"stroke-linejoin="round"><pathd="M11 5 6 9H3v6h3l5 4z"/><pathd="M19 5a9 9 0 0 1 0 14"/></svg>低音增强<spanclass="val">35%</span></div><inputclass="r2"type="range"min="0"max="100"value="35"><divclass="scale"><span>0</span><span>50</span><span>100</span></div></div><divclass="hint">FIREFOX 走 ::-moz-range-progress 原生填充 · 拖到一半松手试试</div></div><divclass="tag">每天一个惊艳 CSS · DAY 40</div><script>// +1 行 JS:拖动时把当前值写进 --v,填充段与拇指实时同步document.querySelectorAll('input[type=range]').forEach(s=>s.addEventListener('input',()=>{s.style.setProperty('--v',s.value+'%');s.closest('.row').querySelector('.val').textContent=s.value+'%';}));</script></body></html>

三、三个核心技法拆解

今天这三个技法,从双体系到对齐到变量接口,一层一层往上走。

技法1:双伪元素体系,webkit与-moz各谈各的

range 的可定制部分分属两套伪元素体系。

Chromium 和 Safari 走 webkit 体系:::-webkit-slider-runnable-track是轨道,::-webkit-slider-thumb是拇指。

Firefox走moz体系:::-moz-range-track是轨道,::-moz-range-progress是已填充段,::-moz-range-thumb是拇指。

两边都要写。缺一边就是半残。

/* webkit 体系 */input[type="range"]::-webkit-slider-runnable-track{/* 轨道 */}input[type="range"]::-webkit-slider-thumb{/* 拇指 */}/* moz 体系 */input[type="range"]::-moz-range-track{/* 轨道 */}input[type="range"]::-moz-range-progress{/* 已填充段 */}input[type="range"]::-moz-range-thumb{/* 拇指 */}

Firefox比webkit多一个“已填充段”伪元素。

::-moz-range-progress是“从最小值到当前值”的那一段,Firefox原生提供,不需要自己画。

webkit没有这个伪元素。

在Chrome里,“已填充段”要靠渐变硬切来做,这是下一个技法的事。

Firefox的-moz-range-progress反而比webkit省事。这是“两套体系各有优劣”的典型案例。

写跨浏览器组件的时候,不要假设一套体系一定比另一套简单,两边都要看。

还有一个必写的属性:appearance: none。

input[type="range"]{-webkit-appearance:none;appearance:none;}

webkit和标准两侧都要置none。否则系统皮肤会叠在手绘层上,你画的轨道、拇指都被系统默认样式盖住。

这是“原生控件换皮”的第一道关。任何input、button、select想自己画样式,都要先appearance: none。


技法2:webkit拇指的margin-top陷阱

这是自定义滑块90% 的“对不齐”问题的根源。

webkit体系里,thumb默认与track顶部对齐。轨道高度是6px,拇指高度是20px。如果不管它,拇指会吊在轨道上方,看起来像“漂浮的圆钮”,不是“嵌在轨道里的滑块”。

要把居中修正手动算回来:

input[type="range"]::-webkit-slider-thumb{width:20px;height:20px;margin-top:-7px;}

margin-top = (轨道高 − 拇指高) / 2。

本例:(6 − 20) / 2 = -7px。

负值把拇指往上提7px,正好让拇指的中心对齐轨道的中心。

不写这行,拇指会吊在轨道上方。

这是自定义滑块的头号问题,也是很多教程一笔带过、新手反复踩的坑。

记住这个公式:

margin-top = (track 高度 − thumb 高度) / 2

轨道6px、拇指20px,就是(6 - 20) / 2 = -7px。

轨道8px、拇指24px,就是(8 - 24) / 2 = -8px。

轨道4px、拇指16px,就是(4 - 16) / 2 = -6px。

这是webkit体系的“数学税”—— 你必须自己算居中,浏览器不会帮你。

moz体系没这个问题,-moz-range-thumb默认就居中。这是两套体系的差异,写的时候要分开处理。


技法3:填充段等于渐变加1行JS实时接口

webkit 没有“已填充段”伪元素,填充段要靠渐变硬切来做:

input[type="range"]::-webkit-slider-runnable-track{height:6px;border-radius:3px;background:linear-gradient(90deg,var(--fill)var(--v),#26303dvar(--v));}

linear-gradient(90deg, var(--fill) var(--v), #26303d var(--v))读一遍:

  • 从0到--v:用--fill颜色(品牌色)
  • 从--v到100%:用#26303d颜色(深灰)

--v是分界点。--v: 60%时,前60%是品牌色,后40%是深灰。

这就是“已填充段”和“未填充段”的视觉分割。

--v从哪来?

默认值写在input[type="range"]上:

input[type="range"]{--v:60%;}input[type="range"].r2{--v:35%;}

拖动时,一行JS把当前值写进去:

s.style.setProperty('--v',s.value+'%');

s.value是当前值(0 到 100),加%变成百分比字符串,写进--v变量。

渐变实时更新,填充段跟着手指走。

这就是“静态是姿态,变量是接口”—— CSS只关心“--v是多少”,JS只关心“--v怎么变”。两者解耦,各司其职。

为什么用1行JS,而不是纯CSS?

因为input[type="range"]的当前值,CSS拿不到。

:valid只知道“是否合法”,不知道“具体是多少”。

attr()读不到value属性,因为用户拖动改变的是“属性值”,不是“HTML上的value属性”。

纯CSS拿不到“输入值的具体数值”,这是CSS的边界。

1行JS就是把这个值暴露给CSS的最短路径。

能少用一行就少用一行,是“组件惊艳化”的态度。


四、踩坑位

① 漏appearance: none

/* 错:系统皮肤叠在手绘层上 */input[type="range"]{/* 没有 appearance */}/* 对:webkit和标准两侧都置none */input[type="range"]{-webkit-appearance:none;appearance:none;}

appearance: none是“原生控件换皮”的第一道关。不写,你画的轨道和拇指都被系统默认样式盖住。

webkit和标准两侧都要置none。这是所有input、button、select自定义样式的通用前提。

② thumb忘margin-top

/* 错:拇指吊在轨道上方,看起来像漂浮的圆钮 */input[type="range"]::-webkit-slider-thumb{width:20px;height:20px;}/* 对:手动修正居中 */input[type="range"]::-webkit-slider-thumb{width:20px;height:20px;margin-top:-7px;}

见技法2,对不齐的头号元凶。

margin-top = (track 高度 − thumb 高度) / 2。

不写这行,拇指会吊在轨道上方。这是自定义滑块90%的“对不齐”问题。

记住这个公式,就不会再踩。

③input自身高度塌陷

/* 错:appearance: none后input高度归零,拇指没有活动区 */input[type="range"]{appearance:none;}/* 对:显式给高度,给拇指留活动区 */input[type="range"]{appearance:none;height:20px;}

appearance: none之后,input自身高度归零。拇指虽然视觉上显示,但可拖动的区域只有0高度,鼠标点不中。

显式给height: 20px,给拇指留活动区。

这是“视觉尺寸”和“可操作尺寸”分离的通用做法。

视觉上轨道只有6px,但input本身要20px高,鼠标好点中。

可操作区域比视觉区域大,是移动端友好的底线。

④ 触控目标太小

/* 错:视觉轨道6px,可拖动区域也只有6px */input[type="range"]{height:6px;}/* 对:视觉6px,可操作20px */input[type="range"]{height:20px;}input[type="range"]::-webkit-slider-runnable-track{height:6px;}

视觉轨道6px没关系,但input的点击区保持20px高,拇指20px。

移动端可拖是底线。手指比鼠标粗,6px的轨道根本点不中。

这是“视觉”和“交互”分离的通用心法—— 视觉可以精致,交互区域必须足够大。

⑤ 拖动时加transition

/* 错:拇指跟手场景下transform过渡会造成拖手延迟 */input[type="range"]::-webkit-slider-thumb{transition:transform .3s;}/* 对:只对box-shadow做过渡,位置不过渡 */input[type="range"]::-webkit-slider-thumb{transition:box-shadow .2s;}

拇指跟手的场景,transform过渡会造成“拖手延迟”。

拖动时,拇指应该跟着鼠标走,不该有过渡。

过渡只用在box-shadow上——按下去的时候外圈扩散,松手的时候收回。

“跟手的东西不能有过渡”—— 这是所有拖拽类交互的通用原则。

⑥ 两套体系只写一套

/* 错:只写webkit,Firefox里滑块还是默认样式 */input[type="range"]::-webkit-slider-runnable-track{/* ... */}/* 对:两套都写 */input[type="range"]::-webkit-slider-runnable-track{/* ... */}input[type="range"]::-moz-range-track{/* ... */}

只写webkit体系,Firefox里滑块还是默认样式。用户切到Firefox,观感崩一半。

两套都写,跨浏览器观感一致。

这是“跨浏览器组件”的通用原则—— 每个浏览器有自己的一套,都要照顾到。


五、变式挑战(10–30分钟,动手才有收获)

任务:竖向滑块。

现代写法一行:

input[type="range"]{writing-mode:vertical-lr;direction:rtl;}

writing-mode: vertical-lr让range竖向书写,direction: rtl让值从下往上。宽高互换、轨道竖走。

老浏览器用-webkit-appearance: slider-vertical兼容,但现在已经很少需要。

效果是一条竖向调音台推子。上下拖动,填充段从下往上。

竖向滑块适合调音台、音量条、亮度条这类“从下往上”的场景。

进阶方向(选做):

一是给拇指加数值气泡。拖动时拇指上方浮出一个气泡显示当前值,气泡位置随--v移动。

.bubble{transform:translateY(calc(-20px -var(--v)* 0.01 * 20px));}

--v是百分比,0.01 * 20px是“每1%移动0.2px”,加起来就是气泡相对拇指的偏移。

对比滑块“分割线位置随--p移动”的同类思路。

二是加“双滑块”。两个拇指在一根轨道上,选一个范围。需要JS处理“哪个拇指在前”,代码量翻倍。

三是加“刻度线”。轨道下方画一排小刻度,每隔10%一个,像调音台上的刻度。用repeating-linear-gradient画。

四是换成“环形滑块”。轨道是一个圆环,拇指沿着圆环转。这是“音量旋钮”的常见做法,和线性滑块是两种不同的交互模型。


六、小结

今天这一篇里藏着6个通用技能点:

技能适用场景
appearance: none清空默认样式所有原生控件自定义
webkit / moz双伪元素体系所有跨浏览器range滑块
margin-top修正拇指居中所有 webkit 滑块
渐变硬切做“已填充段”所有webkit滑块
--v变量做实时接口所有“拖动实时反馈”的控件
视觉尺寸和可操作尺寸分离所有触控友好的控件

最值得记住的三条。

第一,appearance: none是原生控件换皮的第一道关。

不写,你画的轨道、拇指都被系统默认样式盖住。

webkit和标准两侧都要置none。这是所有input、button、select自定义样式的通用前提。

第二,webkit拇指的margin-top是数学税。

margin-top = (track 高度 − thumb 高度) / 2。

不写这行,拇指会吊在轨道上方。这是自定义滑块90%的“对不齐”问题的根源。

记住这个公式,就不会再踩。

第三,静态是姿态,变量是接口。

CSS只关心--v是多少,JS只关心--v怎么变。

1行JS就是把“数值”暴露给CSS的最短路径。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/31 2:15:56

正定算子是线性代数、优化理论、数值计算和机器学习中非常重要的概念

正定算子是线性代数、优化理论、数值计算和机器学习中非常重要的概念。在线性代数中&#xff0c;正定算子通常对应正定矩阵&#xff1b;在泛函分析或量子力学中&#xff0c;它也可以指希尔伯特空间上的正定线性算子。对于有限维实向量空间&#xff0c;一个对称矩阵如果对所有非…

作者头像 李华
网站建设 2026/10/31 2:15:54

酉算子是线性代数和泛函分析中的重要概念,也是量子计算、信号处理和数值线性代数中经常出现的结构

酉算子是线性代数和泛函分析中的重要概念&#xff0c;也是量子计算、信号处理和数值线性代数中经常出现的结构。它的核心思想并不复杂&#xff1a;酉算子是一种“保持内积、保持长度”的线性变换。在有限维情形下&#xff0c;酉算子通常可以用酉矩阵来表示。 设复矩阵 UUU 为 n…

作者头像 李华
网站建设 2026/10/31 2:15:38

迹算子是线性代数中一个基础但非常有用的概念,对于一个方阵,迹等于其主对角线元素之和

迹算子是线性代数中一个基础但非常有用的概念。对于一个方阵&#xff0c;迹等于其主对角线元素之和。它看似简单&#xff0c;却在矩阵分析、机器学习、优化理论、数值计算和物理建模中频繁出现。相比行列式、特征值等概念&#xff0c;迹的计算更稳定、解释更直观&#xff0c;也…

作者头像 李华
网站建设 2026/10/31 2:14:48

力扣128最长连续序列

枚举数组中的每个数 x&#xff0c;考虑以其为起点&#xff0c;不断尝试匹配 x1,x2,⋯ 是否存在假设最长匹配到了 xy&#xff0c;那么以 x 为起点的最长连续序列即为 x,x1,x2,⋯,xy&#xff0c;其长度为 y1&#xff0c;我们不断枚举并更新答案即可class Solution {public int lo…

作者头像 李华
网站建设 2026/10/31 2:14:47

滑坡自然灾害应急监测 房屋损坏监测数据集 AI图像数据集第10834期

文章目录滑坡损毁房屋实例分割数据集滑坡检测&#xff0c;房屋损毁分割&#xff0c;灾后评估&#xff0c;地质灾害识别&#xff0c;无人机灾害监测滑坡损毁房屋实例分割数据集 滑坡灾害房屋损毁识别是自然灾害应急监测中的重要方向&#xff0c;这份滑坡损毁房屋实例分割数据集…

作者头像 李华
网站建设 2026/10/31 2:13:48

C语言结构体:把一堆不同类型的数据打包在一起[特殊字符]

写 C 语言的时候&#xff0c;char、int、float 这些都是基础类型。但现实世界中的信息往往是成套出现的&#xff1a;一个学生&#xff0c;有姓名、年龄、性别、学号&#xff1b;一本书&#xff0c;有书名、作者、价格。总不能单独定义一堆零散变量 name1、age1、sex1……&#…

作者头像 李华