星级评分
星星会填充到你悬停的那一颗,选中的那颗轻轻弹一下,全部基于原生单选按钮。
用纯 CSS 在原生单选按钮上制作五星评分。把每个 input 和一个 SVG 星星包在 label 中,label 用 display: flex 和 flex-direction: row-reverse 排列,并在 DOM 中按 5 到 1 的顺序排列,这样兄弟选择器 ~ 就能作用于视觉上位于左侧的星星。星星初始为弱化色。用 label:has(input:checked) 和 label:has(input:checked) ~ label 把选中的星星及其在 DOM 中之后的星星填充为主色;整行处于悬停时先重置,再用 label:hover 和 label:hover ~ label 填充。星星被选中时播放一个 360ms 的 keyframe:scale 从 1 到 1.35 再回到 1,缓动为 cubic-bezier(0.34, 1.56, 0.64, 1)。input 在视觉上隐藏但保持可聚焦,聚焦时在对应的 label 上显示焦点框。每个 input 设置类似“4 星”的 aria-label。在 prefers-reduced-motion 下去掉弹跳。标题为“给这个动效打分”。
源代码
'use client'
import { useId } from 'react'
import { fmt } from '@/lib/i18n'
import s from './demo.module.css'
export default function StarRating({ labels }: { labels: Record<string, string> }) {
const name = useId()
return (
<fieldset className={s.field}>
<legend className={s.legend}>{labels.legend}</legend>
<div className={s.stars}>
{[5, 4, 3, 2, 1].map((n) => (
<label key={n} className={s.star}>
<input type="radio" name={name} value={n} className={s.input} aria-label={fmt(labels.star, n)} />
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 2.8l2.8 5.9 6.4.8-4.7 4.4 1.2 6.4L12 17.2l-5.7 3.1 1.2-6.4-4.7-4.4 6.4-.8z" />
</svg>
</label>
))}
</div>
</fieldset>
)
}
.field {
display: grid;
justify-items: center;
gap: 12px;
margin: 0;
padding: 0;
border: 0;
}
.legend {
margin-bottom: 12px;
padding: 0;
font: 500 var(--text-sm) / 1 var(--font-body);
color: var(--stage-muted);
text-align: center;
}
.stars {
display: flex;
flex-direction: row-reverse;
gap: 6px;
}
.star {
position: relative;
display: grid;
width: 36px;
height: 36px;
border-radius: 8px;
cursor: pointer;
}
.star svg {
width: 100%;
height: 100%;
fill: var(--stage-rule);
transition: fill var(--dur-micro) var(--ease-out);
}
.input {
position: absolute;
inset: 0;
opacity: 0;
margin: 0;
cursor: pointer;
}
.star:has(.input:checked) svg,
.star:has(.input:checked) ~ .star svg { fill: var(--stage-accent); }
.stars:hover .star svg { fill: var(--stage-rule); }
.stars:hover .star:hover svg,
.stars:hover .star:hover ~ .star svg { fill: var(--stage-accent); }
.star:has(.input:checked) svg { animation: pop 360ms cubic-bezier(0.34, 1.56, 0.64, 1); }
.star:has(.input:focus-visible) { outline: 2px solid var(--color-focus); outline-offset: 2px; }
@keyframes pop {
50% { transform: scale(1.35); }
}
@media (prefers-reduced-motion: reduce) {
.star:has(.input:checked) svg { animation: none; }
}