最稳妥的星级评分方案是用一组name相同的配合嵌套,支持键盘操作、屏幕阅读器及无js表单提交;需隐藏原生控件、用css绘制星星、设required,并按:hover→:focus→:checked顺序编写状态样式。

用 <input type="radio"> 实现可访问的星级评分
纯 HTML 星级评分最稳妥的方式是用一组 <input type="radio">,配合 <label></label> 关联。它天然支持键盘操作(Tab 切换、空格选中)、屏幕阅读器朗读,且无需 JS 就能提交表单值。
常见错误是直接用 <div> + CSS 伪元素模拟星星,结果无法聚焦、不能被读屏软件识别,表单提交时也拿不到值。
<ul>
<li>每颗星必须是独立的 <code><input type="radio">,name 相同(如 name="rating"),value 为 1–5
<label></label> 必须用 for 属性绑定对应 id,或把 <input> 包在 <label></label> 内(推荐后者,更健壮)position: absolute; opacity: 0;),再用 ::before 或背景图绘制星星图标required 属性——否则用户可能跳过评分直接提交CSS 控制星星状态:hover/focus/checked 的优先级怎么写
鼠标悬停显示高亮、键盘聚焦时有轮廓、已选中后保持实心——这三者状态叠加时容易出错,核心是 CSS 选择器权重和顺序。
典型问题是:hover 时星星变黄了,但点选后又变回空心;或者用键盘 tab 进去没反应。
立即学习“前端免费学习笔记(深入)”;
- 状态顺序必须是:
:hover→:focus→:checked,且:checked规则要放在最后,否则会被前面覆盖 - 不要只写
input:hover + label::before,而要用input:focus + label::before, input:hover + label::before合并写,避免重复 - 对已选中的星,用
input:checked ~ label::before(注意是~而非+),因为 label 在 input 后面时,+找不到 - 移动端 touch 设备上,
:hover可能不触发,建议同时加:active和媒体查询@media (hover: none)做降级
为什么不用 <input type="range"> 做星级评分
虽然 <input type="range"> 看似简单,但它不适合星级评分这个场景。
用户看到的是“5 颗星”,不是“0–100 的滑块”。用 range 容易导致:拖到 63% 时不知道对应几颗星,提交值是小数(如 3.7),后端还得四舍五入,还可能误触拖过头。
-
<input type="range">默认步进是 1,但用户仍可能拖到非整数值,需用step="1"强制 - 没有语义化“星级”含义,
aria-label很难准确描述(比如“3 星半”还是“4 星”?) - 移动端滑块精度差,手指一滑就跳变,体验远不如点击单颗星稳定
- 如果真要用,必须配实时数字反馈(如旁边显示
3/5),否则用户完全不确定当前值
JavaScript 补充交互时,别绕过原生表单逻辑
有些团队加 JS 是为了实现“半星”或“鼠标划过实时预览”,但很容易破坏可访问性和表单完整性。
最常踩的坑是:监听 mousemove 改变样式,却没同步更新 checked 状态;或者用 JS 模拟点击,但没触发 change 事件,导致 Vue/React 绑定失效。
- 预览效果(如划过第 3 颗星时前 3 颗高亮)只改样式,不碰
checked—— 真正选中仍靠 click - 如果要支持半星(如 3.5),必须用两个
<input>(整星 + 半星),或改用隐藏的<select></select>,别强行用 radio 模拟 - JS 修改后务必手动触发
input.dispatchEvent(new Event('change', { bubbles: true })),否则框架收不到变更 - 禁用 JS 时,整个评分组件应仍能正常使用(即降级回纯 radio 方案)
真正难的不是画五颗星星,而是让每种输入方式(鼠标、键盘、触摸、读屏)都得到一致反馈。很多项目上线后才发现盲人用户根本没法评分——问题往往出在 label 没关联,或者 focus 样式被 CSS 重置掉了。











