我配了三年视频播放器,每次播到一半状态就乱了——今天 Chrome 152 把这件事彻底原生化了
写过三年视频播放器的,大概都被状态同步折磨过——播着播着,图标还显示播放中,但视频已经停了;或者点了一下暂停,但缓冲动画还在转。
根子不在代码,在于我们一直在用 JavaScript 维持一份”影子状态”:浏览器自己知道视频在不在播放,我们偏要自己再记一份,然后靠事件监听器来回同步——这层同步从来不是无代价的。
Chrome 152 给你把这个中间层彻底删了。
七个伪类,把浏览器自己知道的状态直接交给 CSS
Chrome 152 稳定版 8 月 25 日发布,随之而来的是七个媒体元素伪类:
:playing — 正在播放(不包括缓冲和卡顿导致的暂停)
:paused — 暂停
:seeking — 用户正在拖动进度条
:buffering — 正在缓冲(视频还在,但数据跟不上了)
:stalled — 完全卡住(网络彻底断了)
:muted — 静音
:volume-locked — 音量被系统锁死(比如 iOS)
这七个状态,浏览器一直知道,之前只有 JavaScript 能读到,现在 CSS 自己会匹配了。
真正的用法不是 video:paused,是 :has()
这是官方发布会略过、但真正让这个功能可用的关键。
video:paused 只能给 <video> 本身加样式——但实际项目中,99% 的场景是你要给播放按钮、遮罩层、缓冲动画这些兄弟元素加样式,而不是给视频本身加。
CSS :has() 解决了这个问题:
/* 单用:只能样式视频本身,用处不大 */
video:paused { opacity: 0.8; }
/* 真正的用法:容器查询子元素状态,然后样式容器内任意元素 */
.player:has(video:paused) .play-icon { display: block; }
.player:has(video:playing) .play-icon { display: none; }
这就是整个技巧的核心——用 :has() 做桥,把媒体元素的状态传给容器,容器再样式容器内任意子元素。整个过程没有 JavaScript,没有 classList,没有事件监听器。
一个完整播放器的样式,不靠一个 JS 事件监听器
先看结构,零状态属性(无 data-playing,无状态类名):
<div class="player">
<video src="/video.mp4" poster="/poster.jpg" preload="metadata" playsinline></video>
<div class="controls">
<button class="toggle" type="button" aria-pressed="false">
<svg class="icon-play"><use href="#play" /></svg>
<svg class="icon-pause"><use href="#pause" /></svg>
<span class="sr-only">Play</span>
</button>
<button class="mute" type="button" aria-pressed="false">
<svg class="icon-sound"><use href="#sound" /></svg>
<svg class="icon-muted"><use href="#muted" /></svg>
<span class="sr-only">Mute</span>
</button>
</div>
<div class="spinner" aria-hidden="true"></div>
</div>
然后是 CSS——原来需要五个 JavaScript 事件监听器的逻辑:
.player { position: relative; container-type: inline-size; }
/* 图标切换 */
.icon-play, .icon-pause { display: none; }
.player:has(video:paused) .icon-play { display: block; }
.player:has(video:playing) .icon-pause { display: block; }
/* 静音状态 */
.icon-sound, .icon-muted { display: none; }
.player:has(video:muted) .icon-muted { display: block; }
.player:has(video:not(:muted)) .icon-sound { display: block; }
/* 系统锁音量时隐藏静音按钮——iOS 会锁,用户点了也没反应 */
.player:has(video:volume-locked) .mute { display: none; }
/* 缓冲动画 */
.spinner { opacity: 0; transition: opacity 150ms; pointer-events: none; }
.player:has(video:buffering) .spinner { opacity: 1; }
/* 卡住(网络断了)vs 缓冲——卡住时动画也要停 */
.player:has(video:stalled) .spinner {
opacity: 1;
animation-play-state: paused;
}
/* 拖进度条时画面变暗,让拖动意图更明确 */
.player:has(video:seeking) video { filter: brightness(0.7); }
/* 播放时自动隐藏控制栏,悬停/聚焦时显示 */
.controls { opacity: 1; transition: opacity 200ms; }
.player:has(video:playing) .controls { opacity: 0; }
.player:has(video:playing):hover .controls,
.player:has(video:playing):focus-within .controls { opacity: 1; }
最后那条是我觉得最有价值的一行。自动隐藏控制栏——原来要 setInterval,要 mousemove 监听,要清除定时器,要处理边界情况,是内存泄漏的经典重灾区。现在四行 CSS,键盘用户的 :focus-within 处理还是附赠的,JavaScript 版本通常会漏掉这个。
浏览器支持:Safari 早就有了,Chrome 现在才来
这是好消息:Safari 15.4(2022 年)就有了,Firefox 150(2026 年 4 月)也上了。
Chrome 152 是最后一个入场的大浏览器——但入场之后,这就是全量可用的功能。CanIUse 数据显示 Safari/iOS 15.4+ 和 Firefox 150+ 已经覆盖了不少用户。
但”某浏览器不支持”还是要处理的:
/* 兜底:靠 JS 设置 data 属性,驱动传统 class 方案 */
.player[data-state='paused'] .icon-play { display: block; }
.player[data-state='playing'] .icon-pause { display: block; }
/* 增强:浏览器支持就直接用原生状态 */
@supports selector(video:playing) {
.player:has(video:paused) .icon-play { display: block; }
.player:has(video:playing) .icon-pause { display: block; }
/* 其他增强规则全在里面 */
}
只有支持的浏览器才会走 @supports 里面那套,其他浏览器继续用 JS 驱动 data 属性的兜底方案。渐进增强,不需要特征检测库。
真正删掉的是什么
“零 JavaScript” 是个噱头式标题,实际账要算清楚。
删掉的: 所有”把媒体状态同步到 class”的监听器——play、pause、waiting、playing、volumechange,这些是维护成本最高、最容易出现同步 bug 的部分。
还在的:
// 播放/暂停动作——CSS 无法触发行为,只能靠 JS
toggle.addEventListener('click', () => {
video.paused ? video.play() : video.pause()
})
// 无障碍状态——CSS 改不了 aria 属性,屏幕阅读器用户要看按钮状态
const syncAria = () => {
toggle.setAttribute('aria-pressed', String(!video.paused))
toggle.querySelector('.sr-only').textContent = video.paused ? 'Play' : 'Pause'
}
video.addEventListener('play', syncAria)
video.addEventListener('pause', syncAria)
实际项目中,原来 8–10 个监听器变成了 2 个,而且这两个是语义层面的(触发行为、更新无障碍属性),不是”维持影子状态”层面的。这才是真正的变化。
Chrome 152 正式版 8 月 25 日上线。如果你在维护一个自定义播放器,先加 @supports selector(video:playing) 做渐进增强,原有 JS 兜底留着不动——三个月后,等覆盖率再上来,把兜底删掉就行。
评论区
登录后可评论。