写过播放器的都踩过这个坑——播放状态一变就要写一堆JS事件,Safari 支持了四年没人敢用,今天 CSS 把它彻底原生化了

写过自定义播放器的都踩过这个坑——播放状态一变,就要写一堆 JS 事件监听器:play 事件要改图标、pause 事件要改图标、waiting 事件要转圈、muted 变化要切图标。每次加个状态,都要去找对应的事件、防止重复绑定、小心内存泄漏。

今天这件事,被七个 CSS 伪类彻底原生化了。

七个状态,一行选择器

/* 播放中:进度条高亮、绿色播放键 */
video:playing { border: 3px solid #22c55e; }

/* 暂停:灰色遮罩 */
video:paused { opacity: 0.8; }

/* 缓冲中:显示转圈 */
video:buffering { animation: spin 1s linear infinite; }

/* 跳转中:快进快退按钮高亮 */
video:seeking { cursor: grabbing; }

/* 静音:喇叭加斜线 */
video:muted + .volume-icon::before { content: "🔇"; }

/* 卡住:提示重新加载 */
video:stalled { outline: 2px dashed red; }

/* 音量锁定:灰掉音量条 */
video:volume-locked { pointer-events: none; opacity: 0.5; }

这七个伪类直接读浏览器内部的媒体状态,不需要任何 JS 桥接:

伪类 触发条件
:playing 正在播放(包含 buffering/stalled 暂停但视为 playing)
:paused 用户暂停或未激活
:buffering 播放中但数据不足,正在缓冲
:stalled 试图播放但没有任何数据到达
:seeking 用户在跳转进度
:muted 静音状态
:volume-locked 音量被系统锁定(如 iOS AVRoutePickerView)

以前的做法 vs 今天

以前:每个状态都要 JS 事件驱动

const video = document.querySelector("video");
const icon = document.querySelector(".play-icon");

// 5 个事件,每个都要清理函数
video.addEventListener("play", () => (icon.textContent = "⏸"));
video.addEventListener("pause", () => (icon.textContent = "▶"));
video.addEventListener("waiting", () => (icon.textContent = "⏳"));
video.addEventListener("volumechange", () => {
  icon.textContent = video.muted ? "🔇" : "🔊";
});
// ……3 个事件再加更多监听器

今天:一行 CSS

.play-icon::before { content: "▶"; }
video:playing ~ .play-icon::before { content: "⏸"; }
video:paused ~ .play-icon::before { content: "▶"; }
video:buffering ~ .play-icon::before { content: "⏳"; }
video:muted ~ .play-icon::before { content: "🔇"; }

没有事件监听器,没有状态变量,没有清理函数。浏览器状态变化时,CSS 自动响应。

真实场景:自定义播放控件

<video src="movie.mp4" controls></video>
<div class="controls">
  <button class="play-btn">▶</button>
  <div class="progress"></div>
  <button class="mute-btn">🔊</button>
</div>
/* 播放时控件自动淡出 */
video:playing ~ .controls { opacity: 0.3; transition: opacity 0.3s; }
video:playing ~ .controls:hover { opacity: 1; }

/* 缓冲时进度条变半透明 */
video:buffering ~ .controls .progress { opacity: 0.5; }

/* 静音时喇叭图标变 */
video:muted ~ .controls .mute-btn::before { content: "🔇"; }

/* 跳转时整个控件变红 */
video:seeking ~ .controls { background: rgba(239, 68, 68, 0.1); }

兼容性:Safari 领先四年,2026 年全面覆盖

这是少数 Safari 领先业界多年的案例:

浏览器 首次支持版本 支持时间
Safari / iOS Safari 15.4 2022 年 3 月
Firefox 150 2026 年 4 月
Chrome / Edge 152 2026 年 8 月

Firefox 150 是今年 4 月发的,Chrome 152 是今年 8 月 25 日刚发。Safari 早在 2022 年就有这个能力,只是其他浏览器生态一直跟不上,导致很多团队不敢用。

2026 年 8 月 28 日,这个功能正式进入 Baseline——意味着全球 90%+ 的浏览器都支持了。

三个要注意的坑

1. :playing:buffering 不互斥

:buffering 发生时元素仍处于 :playing 状态(因为它在播放中,只是数据不够)。所以样式叠加时,:playing:buffering 可以同时生效。

2. :paused 不等于「未播放」

加载完成但还没点播放时,也是 :paused 状态。如果要区分「用户暂停」和「未开始播放」,需要额外 JS 标记。

3. 静音检测用 :muted 不用 audio 属性

<audio muted> 会在加载时就是静音状态,:muted 会匹配,:paused 也会匹配。区分方式:加一个初始 class,在 JS 里用 audio.paused && audio.readyState >= 3 判断是否真的未开始。

下一步

如果你的项目里有自定义播放器,先把事件监听器方案检查一遍——大概率可以用这几个伪类简化掉。Safari 用户已经享受了四年,Chrome/Firefox 用户从 2026 年开始也能用上了。

/* 先做渐进增强 */
@supports (selector(:playing)) {
  video:playing + .controls { /* 新的样式 */ }
}

现在 CSS 能感知媒体播放状态了——播放器控件再也不用 JS 来回同步了。

评论区

0 条评论

登录后可评论。

阿柯·前端架构 16 阅读