19

评分组件想必大家都碰到过了吧,纯 CSS 方式网上也有很多实现,一般都是通过 input[type="radio"] 实现,比如这个

大致原理如下

  1. 通过 flex-direction: row-reverse 或者其他手段(dirction:rtltransform:scaleX(-1)),将元素位置翻转
  2. 配合 :checked~ 选中视觉上的前面兄弟节点

实现非常精妙,无需 js 接入,兼容性也不错,不过这里还有两个瑕疵

  1. 视觉展示和页面结构不一致,比如给每个 input 添加 value 属性,需要倒序来加,不符合一般人的认知
<div class="raido-group">
    <input type="radio" name="rate" value="5">
    <input type="radio" name="rate" value="4">
    <input type="radio" name="rate" value="3">
    <input type="radio" name="rate" value="2">
    <input type="radio" name="rate" value="1">
</div>
  1. 当组件聚焦后,通过键盘左右键切换也是相反的,具体表现就是,按方向键 “右”,评分却减少,反之亦然

优化

上面提到的两个瑕疵其实都是元素位置翻转引起的,目的也是为了实现视觉上的前置兄弟节点选择器,有没有什么办法规避这个问题呢?答案就是重置,处理如下

  1. 默认情况下都是选中的样式
  2. 配合 :checked~ 选中后面的元素,样式设置为未选中的样式

具体实现如下,html 为正常结构

<div class="raido-group">
    <input type="radio" name="rate" value="1">
    <input type="radio" name="rate" value="2">
    <input type="radio" name="rate" value="3">
    <input type="radio" name="rate" value="4">
    <input type="radio" name="rate" value="5">
</div>

下面是关键样式

[type="radio"]{
    -webkit-appearance: none;
    width: 20px;
    height: 20px;
    -webkit-mask: url("data:image/svg+xml,%3Csvg width='12' height='11' viewBox='0 0 12 11' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6 0l1.693 3.67 4.013.476L8.74 6.89l.788 3.964L6 8.88l-3.527 1.974.788-3.964L.294 4.146l4.013-.476L6 0z' fill='%23F67600'/%3E%3C/svg%3E");
    -webkit-mask-size: 20px;
    -webkit-mask-repeat: no-repeat;
    -webkit-mask-position: center;
    background-color: coral; /*默认是选中的样式,橙色*/
    margin: 0;
    transition: .2s;
}

[type="radio"]:checked~[type="radio"]{
    background-color: #E8EAED; /*未选中的样式,灰色*/
}
这里的星星图形是通过 mask 实现的,根据实际需求也可采用传统背景图片的方式

可以看到优化后的方案代码更加精简了,html 也符合常规认知,键盘切换也正常了

当然这个实现也还是有点瑕疵的,由于默认是选中状态,所以即使没有任何 :checked, 也会是全部高亮(满分)的情况,不会有0分的情况,所以推荐默认把最后一项设置 checked 属性。当然实际场景也并不会有0分的选项,很多场景下都是默认满分,所以这个小问题无伤大雅~

半颗星的实现

在上面的基础上,半颗星也很容易,只需要使用 10 个 input,每个只占据一半的空间即可

  1. 10个input
  2. 每个input宽度为之前的一半
  3. 配合 nth-child(odd)nth-child(even)设置显示区域

具体实现如下

<div class="raido-group rate-half">
    <input type="radio" name="rate-half" value="0.5">
    <input type="radio" name="rate-half" value="1">
    <input type="radio" name="rate-half" value="1.5">
    <input type="radio" name="rate-half" value="2">
    <input type="radio" name="rate-half" value="2.5">
    <input type="radio" name="rate-half" value="3">
    <input type="radio" name="rate-half" value="3.5">
    <input type="radio" name="rate-half" value="4">
    <input type="radio" name="rate-half" value="4.5">
    <input type="radio" name="rate-half" value="5">
</div>

下面是关键样式

.rate-half [type="radio"]{
    width: 10px; /* 宽度设置一半 */
}
.rate-half [type="radio"]:nth-child(odd){
    -webkit-mask-position: left; /* 设置星星的显示区域 */
}
.rate-half [type="radio"]:nth-child(even){
    -webkit-mask-position: right;
}

以上源码可查看 codepen

更好的实现

上面的实现已经非常完美了,无需 js,兼容性也不错,但是还是有些需要改进的地方,比如

  1. html 修改起来比较麻烦,从 5 分制改成 10 分制需要改动每一个 inputvalue 属性
  2. 星星的数量修改起来比较麻烦,从 5 颗星改成 10 颗星,需要手动添加新的 input 节点
  3. js 获取当前值相对比较麻烦,需要使用 document.quertSelector([type="radio"][name="rate"]:checked).value,不太符合语意,新手可能不太熟悉

那么,还有没有其他方式可以实现类似的效果呢?可以这样想一下,评分组件本质上也是一个表单输入组件,不考虑样式的话,普通的 input 也完全可以实现同样的输入功能,只是需要手动键盘输入而已。那有没有不需要手动输入的只需要简单拖拽就可以完成表单输入的呢?答案就是 input range范围选择器

<input type="range">

范围选择器有一些默认值

  1. 最小值 min0
  2. 最大值 max100
  3. 默认值 value 为区域范围的一半,50
  4. 默认步长 step1

现在和上面的评分组件对照起来,就可以很容易得出下面的结构

<input type="range" max="5" value="0">

这是一个可以输入 0~5 整数的组件,不能有小数出现,功能已经完全满足了

input range 的 shadow dom

现在可以开始改造样式了,在开始之前,可以在 Chrome 中开启 shadow-dom 的显示,方式为在控制台右上角 setting > preferences > Elements,勾选 Show user agent shadow DOM (已经勾选过的可以忽略)

这时便可很清楚的看到 input range 的内部结构

这里一共有三层,分别对应的选择器如下

  1. 容器 input[type="range" i]::-webkit-slider-container
  2. 轨道 input[type="range" i]::-webkit-slider-runnable-track
  3. 滑块 input[type="range" i]::-webkit-slider-thumb

input range 自定义样式

很多时候默认样式不是我们需要的,而且也不易修改,比如 Chrome 中 input range 的已滑动蓝色区域,这部分是无法修改的(Firefox 可以,这里只针对于 Chrome,其他浏览器原理类似),首先要做的就是重置

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

然后,由于滑块也是占据空间的,为了消除这个影响,可以将宽度设置为 0,当然也需要重置默认样式

input[type="range" i]::-webkit-slider-thumb {
    -webkit-appearance: none;
    width: 0;
}

由于 Chrome 没有已滑动区域和未滑动区域的区分,这里采用足够大的 box-shadow 来覆盖实现

input[type="range" i]::-webkit-slider-thumb {
    box-shadow: 999px 0px 0px 999px #E8EAED; /*可以实现一个右侧的足够大的阴影*/
}

通过以上几步可以实现一个这样的效果,可以先停下来体会一下?

最后一步,加上 mask 遮罩,实现镂空效果

input[type="range" i]::-webkit-slider-runnable-track {
    -webkit-mask: url("data:image/svg+xml,%3Csvg width='12' height='11' viewBox='0 0 12 11' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6 0l1.693 3.67 4.013.476L8.74 6.89l.788 3.964L6 8.88l-3.527 1.974.788-3.964L.294 4.146l4.013-.476L6 0z' fill='%23F67600'/%3E%3C/svg%3E"); /*星星图案*/
    -webkit-mask-size: 20px;
    -webkit-mask-repeat: repeat-x;
}

用一张动图描述一下效果

以上源码可查看 codepen

相关扩展

那么,现在来扩展一下,比如实现一个 10 颗星,总分 100 分的评分组件,支持半颗星,实现如下

<input type="range" max="100" value="0" step="5">

还需要修改一下宽度

input[type="range"]{
    width: 200px; /*每个星星的尺寸为20 * 20,10颗星宽度就是200*/
}

这样就得到了一个 10 颗心的评分组件

有时候,如果需要仅仅作为展示,比如一些电影评分,添加 disabled 就可以了~

<input type="range" value="0" step="0.5" disabled>

Firefox 的兼容

Firefox 也可以采用类似的原理,只需要换上对应的选择器即可,如下

  1. 容器 input[type=range](没有单独的容器,用最外层代替)
  2. 轨道 input[type=range]::-moz-range-track
  3. 滑块 input[type=range]::-moz-range-thumb

但是,Firefox 还有一个表示进度的选择器 ::-moz-range-progress,这样就可以不用 box-shadow 来遮盖了,所以,针对 Firefox 的另一种实现方式如下

input[type=range]{
    -webkit-mask: url("data:image/svg+xml,%3Csvg width='12' height='11' viewBox='0 0 12 11' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6 0l1.693 3.67 4.013.476L8.74 6.89l.788 3.964L6 8.88l-3.527 1.974.788-3.964L.294 4.146l4.013-.476L6 0z' fill='%23F67600'/%3E%3C/svg%3E");
    -webkit-mask-size: 20px;
    -webkit-mask-repeat: repeat-x;
    height: 20px;
}
input[type=range]::-moz-range-track{
    background: #E8EAED;
    height: inherit;
}
input[type=range]::-moz-range-progress {
    background: coral;
    height: inherit;
}
input[type=range]::-moz-range-thumb {
    width: 0;
    opacity: 0;
}

动图演示如下

可以看到这里是单独的两层,和 Chrome 用阴影遮挡可不一样,因此可以实现更加灵活的效果,比如不依赖 mask 遮罩,分别设置不同的背景,下面是示意代码

input[type=range]::-moz-range-track{
    background: 未选中的星星;
    height: inherit;
}
input[type=range]::-moz-range-progress {
    background: 选中的星星;
    height: inherit;
}

这样,在 Firefox 中这样的效果也能实现(前提是上面的可以遮挡住下面的)

最后

首先说说这个方式的优点

  1. html 结构非常简单,就一个 input 元素
  2. 属性修改非常方便,只用设置 stepmax等相关属性即可
  3. 语义非常友好,默认表单提交也天然支持
  4. 当然键盘切换也是没有问题
  5. 移动端支持友好,可以滑动选择
  6. js 获取评分也符合常规表单,只需要 input.value 即可

当然,还是有一些局限

  1. IE 支持不够友好,理论上也是可以兼容的,尝试了一下在滑动的过程中会闪烁,体验不佳
  2. 没有 hover 样式

所以,如果你的项目不需要关照 IE ,大可以使用 input range 的方式~


XboxYan
18.1k 声望14.1k 粉丝