播了八年视频播放器,今天才发现浏览器自己认得「播没播」这件事——:playing/:paused 把播放状态的账彻底原生化了
写视频播放器的时候,判断当前「播没播」这件事,以前只能靠 JS 在 video 元素上绑事件、读状态、然后往父容器写 class,再让 CSS 读 class 变样式。一整套下来,状态同步的时机稍有偏差,播放按钮和进度条就会和实际不符。
Chrome 152 把这件事彻底变了——CSS 媒体状态伪类(media element pseudo-classes)正式上线,:playing、:paused、:buffering、:stalled、:seeking、:muted、:volume-locked 这七个选择器,浏览器自己认得媒体元素的播放状态,CSS 直接读,不需要 JS 写任何 class。
播没播,CSS 自己会看
核心用法是配合 :has(),在父容器上监听子 video 的状态:
/* 播放时把按钮半透明 */
.player:has(video:playing) .play-button {
opacity: 0.4;
}
/* 暂停时按钮完全不透明 */
.player:has(video:paused) .play-button {
opacity: 1;
}
这行 CSS 替换了以前一套 JS:监听 play/pause 事件、动态加 class/删 class、还要处理自动播放被浏览器拦截的边界情况。现在浏览器自己知道 video 在不在播,CSS 直接响应,没有中间层。
加载中、卡住、音量锁定——都有对应的
缓冲和卡顿时需要显示加载指示器:
/* 缓冲中或播放卡住,显示 loading */
.player:has(video:buffering, video:stalled) .spinner {
opacity: 1;
}
静音和音量被系统锁定时的 UI 反馈:
/* 静音状态,高亮静音按钮 */
.player:has(video:muted) .mute-btn {
background: #333;
color: #fff;
}
进度条拖动中的提示:
/* 正在 seek,进度条变细 */
.player:has(video:seeking) .progress-bar {
height: 3px;
}
七个状态,覆盖了播放器最常见的所有视觉反馈场景。
为什么比 JS 同步 class 靠谱
以前的模式:video 状态改变 → JS 读事件 → 写 class 到 DOM → CSS 读 class 更新样式。三步走,每一步都有可能滞后或丢事件。比如浏览器自动播放被拦截、play() 返回 Promise 被 reject、或者用户点了浏览器内置的媒体控制条——这些情况 JS 事件不一定触发,class 不同步,UI 就卡在错误状态。
媒体状态伪类把这个中间层删了。CSS 直接问浏览器:「这个 video 现在是 :playing 还是 :paused?」浏览器回答的是实时真相,不是 JS 维护的某个变量的值。
用 :is() 减少重复
如果需要同时对多个状态做同一件事,用 :is() 合并选择器:
/* 播了或者卡住了,进度条都要高亮 */
.player:has(video:is(:playing, :stalled)) .progress-bar {
border-color: #4a90d9;
}
兼容性
这是 Interop 2026 的重点方向之一。Safari 从 15.4(2022年)就支持了,Firefox 从 150(2026年4月)支持,Chrome 152(2026年8月)正式上线,三大引擎全部完成。Chrome 平台状态页面显示 0% 页面加载量,说明大多数开发者还不知道这个功能。
Chrome 152 之前需要 JS 同步 class 的场景,现在可以全部重写成纯 CSS 了。
下一步
如果你现在正在做播放器组件,找一个用 JS 监听 play/pause 事件同步按钮显隐的地方,把它换成 CSS 的 :has(video:playing) 和 :has(video:paused)。从这一个改动开始,感受一下状态同步零时差的体验。
评论区
登录后可评论。