纯CSS实现星级评分组件,不用JS怎么玩出花?

2,886字
12–18 分钟
in

前端圈里星级评分组件都快被玩烂了,大多时候得靠JS来联动值和样式。但有没有想过,光用CSS就能搞定一个能点能选的评分条?其实一个<input type="range">元素就藏着这个骚操作,配上几行样式代码,直接变身五颗金星,连半星评分都不在话下。下面就把这套纯CSS方案拆开揉碎,从原理到实操一步步盘清楚。

目录

核心材料

<input type="range">这个滑动条天生就能选数值,minmax框住了边界。把它变成星星评分,本质上就是给滑动条换皮:把滑轨遮成五颗星,再把滑块伪装成染色刷。滑块滑到第几颗星,就把前面所有星星刷成金色。

整个改造只用CSS,不碰JS,HTML里孤零零一个元素就够:

<input type="range" min="1" max="5">

接下来所有魔法都发生在样式表里。

主元素塑形

滑动条本体要先长成星星底板的模样。宽高比得跟星星数量挂钩,五颗星就用aspect-ratio:5,这样宽度是高度的五倍。高度用自定义属性--s控制,方便以后调大小。

input[type="range"] {
  --s: 100px;          /* 每颗星的尺寸 */
  height: var(--s);
  aspect-ratio: 5;
  appearance: none;    /* 扒掉浏览器默认皮肤 */
}

appearance: none这一步很关键,不同浏览器下原生滑动条的样式五花八门,去掉之后才能从头画。但有个副作用——键盘聚焦时的外轮廓也没了,后面再想办法补回来。

星星的形状怎么来?用mask属性。可以塞一个SVG做遮罩,也可以用渐变拼出来。渐变方案里,用repeating-linear-gradientmask-image配合mask-size: var(--s),让星星图案水平重复五次。SVG方案代码更短,但渐变方案在某些场景更灵活。这里给出渐变写法:

input[type="range"] {
  mask-image: repeating-linear-gradient(90deg, #000 0 calc(100% / 5), transparent 0);
  mask-size: var(--s);
}

实际上真正做星星形状得用更精细的径向渐变或mask-composite,原文给出的demo里用了更复杂的遮罩组合,但核心就是让滑轨区域露出星星轮廓。

滑块变身刷子

滑块是那个可以拖拽的小圆钮,现在要把它变成一把刷子——滑到哪,就把哪边的星星涂成金色。先让滑块瘦成一条1px宽的透明线,这样不会挡住星星图案。

input[type="range"]::-webkit-slider-thumb {
  width: 1px;
  height: var(--s);
  appearance: none;
}

不同浏览器对伪元素的支持不一样,Chrome、Edge认::-webkit-slider-thumb,Firefox认::-moz-range-thumb。得把样式复制两份,不能写在一起,否则整个选择器会废掉。

滑块变刷子的核心武器是border-image。用线性渐变做颜料,然后通过border-imageoutset扩展让颜色溢出到滑块两侧。比如想刷满五颗星,就设置一个超大的横向扩展值(如500px),渐变里的颜色分界点设置在50% + var(--s)/2,保证第一块颜色覆盖到当前选中位置左边的所有区域。

input[type="range"]::-webkit-slider-thumb {
  width: 1px;
  border-image: conic-gradient(at calc(50% + var(--s) / 2), gold 50%, grey 0) fill 0 // var(--s) 500px;
  appearance: none;
}

这段代码里conic-gradient在指定锚点处切出两种颜色,fill 0 // var(--s) 500px表示上下不扩展,左右各扩展500px(远大于组件宽度)。滑块移到第3颗星位置时,渐变的颜色交界线正好卡在第3颗星边缘,左边金色右边灰色,看起来就是前面三颗星全亮。

位置校准让星星对齐

默认情况下滑块的移动范围是从滑轨最左到最右,会导致滑块跑到星星边缘而不是中心。为了让滑块始终对准每颗星的中心,需要在主元素左右加内边距,大小为半颗星宽calc(var(--s) / 2),同时把box-sizing设为border-box,这样内边距不会撑大总宽。

input[type="range"] {
  padding-inline: calc(var(--s) / 2);
  box-sizing: border-box;
}

这样一来滑块的活动范围被压缩到中间区域,每次点击星星时滑块正好落在该星星正中央。

半星精度扩展

很多场景需要半星评分,比如3.5分。把step属性改成0.5,min也改成0.5,就能让滑块每次移动半格。CSS这边对应调整内边距和渐变偏移量,从半颗星宽改成四分之一星宽。

<input type="range" min="0.5" step="0.5" max="5">
input[type="range"] {
  --_s: calc(0.5 * var(--s) / 2);
  padding-inline: var(--_s);
}
input[type="range"]::-webkit-slider-thumb {
  border-image: conic-gradient(at calc(50% + var(--_s)), gold 50%, grey 0) fill 0 // var(--s) 500px;
}

attr()函数甚至可以动态读取stepmax属性,写一个通用模板,改HTML属性就能切换整星或半星模式。不过attr()在非Chrome浏览器里支持还不太稳,得留好回退样式。

键盘操作别丢焦点

去掉原生样式后,键盘Tab聚焦到滑动条上看不到外轮廓,视障用户就懵了。解决办法有两种:套一个外层<span>,用:has(:focus-visible)给外层加outline;或者在遮罩上做手脚,保留一个小区域不遮住,让浏览器默认轮廓能露出来。第二种方法更干净,不增加DOM元素,但遮罩配置会复杂些。

/* 外层包裹方案 */
span:has(:focus-visible) {
  outline: 2px solid blue;
}

换形状玩出更多花样

这套方案不只做星星,换一下mask-image的素材,立马变身爱心评分、蝴蝶评分、信号强度评分。比如用爱心SVG:

input[type="range"] {
  mask-image: url('heart.svg');
  mask-size: var(--s);
}

甚至不需要矢量图,一张PNG透明底图也能当遮罩。音量控制那种阶梯条也能做,用重复的矩形渐变当遮罩就行。

形状类型遮罩方式适用场景
星星渐变或SVG常见评分
爱心SVG或PNG点赞收藏
信号格多层渐变音量信号

动手试一下,改改mask-imageborder-image里的颜色值,一套代码变出几十种组件。这波操作下来,纯CSS也能把交互组件玩得飞起,以后遇到评分条直接一个input搞定,省掉一堆JS监听。