配了三年视频播放器,今天才发现它的「状态层」全删了也没事——Chrome 152 把这件事彻底原生化了

配了三年视频播放器,今天才发现它的「状态层」全删了也没事——Chrome 152 把这件事彻底原生化了

你写一个自定义播放器,第一件事永远是绑一堆事件监听器:play、pause、waiting、playing、volumechange、seeked,然后往容器上 addClass、removeClass。这是每个前端工程师写自定义播放器都要走的流程,不走不行,因为 CSS 读不到 video 标签的播放状态。

但这个流程从今天开始可以彻底不用了。

Chrome 152 正式支持了七个媒体状态伪类,让 CSS 直接读取 video 和 audio 的实时状态::playing(正在播放)、:paused(已暂停)、:buffering(正在缓冲)、:seeking(用户拖动进度条)、:stalled(网络断了)、:muted(静音)、:volume-locked(音量被系统锁定)。浏览器本来就知道自己在播什么,我们以前只是没权限读。

七个状态,一行选择器就能读到

video:playing   /* 视频正在播放 */
video:paused    /* 视频已暂停 */
video:buffering /* 视频在缓冲,但没停 */
video:seeking   /* 用户正在拖进度条 */
video:stalled   /* 网络断了播不动 */
video:muted     /* 已静音 */
video:volume-locked /* iOS 系统锁了音量 */

这七个选择器读的是浏览器引擎里的真实状态,不是 JS 同步过来的副本,不存在”事件晚到一步 UI 就对不上”的问题。

真正的用法是用 :has() 隔代读写

单独写 video:paused 只能样式化 video 本身,但播放器里真正要样式化的是播放按钮、缓冲转圈、静音图标——这些是 video 的兄弟元素,不是 video 本身。:has() 解决了这个问题:

/* 播放/暂停图标切换 */
.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; }

/* 缓冲转圈 */
.spinner { opacity: 0; transition: opacity 150ms; }
.player:has(video:buffering) .spinner { opacity: 1; }

/* 拖进度条时画面变暗,用户知道自己在操作 */
.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; }

注意这里没有任何 classList.add("is-playing"),没有任何 addEventListener("play"),没有任何状态同步。CSS 自己读,CSS 自己样式化。

一个真实的删减案例

媒体工作室 Mintec 在 2026 年第二季度对他们的播放器做了技术审计,发现自定义播放器的状态管理层有 250 行 JavaScript——专门用来把 video 的七个状态同步到容器元素的 className 上。他们把这 250 行全删了,用媒体伪类重写之后,实际代码量变成了零行状态 JavaScript。不是 50 行,不是 20 行,是零行。

这不是特例,这是这类需求的普遍特征:你要做的不过是在视频暂停时把播放图标换成暂停图标,在缓冲时显示转圈——这是纯粹的 UI 状态问题,本来就不应该用 JS 管理。

浏览器支持:2026 年终于全到齐了

这是关键变化。Safari 从 15.4 开始支持,Firefox 从 150 开始支持,Chrome 在 152(2026 年 8 月 25 日稳定版发布)正式加入,Edge 同步跟进。Safari 和 Firefox 已经支持了一年以上,Chrome 152 是最后一块拼图。

目前全球支持率大约 16%(caniuse 数据),还不是 Baseline——这意味着生产环境使用时建议保留渐进增强:用 @supports selector(video:playing) 做特性检测,还不支持的浏览器继续走原来的 JS 路径。npm 上也有社区提供的 polyfill(css-media-pseudo-polyfill),可覆盖其中六个(:volume-locked 暂不支持 polyfill)。

:volume-locked 是个特殊状态

当 iOS 或其他平台因为自动播放政策锁住音量时,视频既不是 :muted 也不是 :playing,而是 :volume-locked。这个状态告诉你不要显示音量调节按钮——用户根本调不了,暴露出来是体验问题。这个状态只有 iOS Safari 上会触发,是唯一一个 polyfill 补不来的伪类。

下一步:数一数你的播放器状态行数

打开你现在的播放器代码,搜一下 addEventListener.*playclassList.add.*playingclassList.toggle,数一数这类状态同步代码有多少行。如果超过 50 行,媒体伪类已经值得迁移了——不是全部重写,是把状态层这部分逐步替换成 CSS 选择器,JS 只保留播放/暂停/音量调节的具体操作逻辑。

Chrome 152 8 月 25 日正式发布。这个节点之后,跨浏览器一致性问题基本不存在了。

评论区

0 条评论

登录后可评论。

小鹿·界面实验室 135 阅读