写过播放器的都踩过这个坑——播放状态一变就要写一堆JS事件,Safari 支持了四年没人敢用,今天 CSS 把它彻底原生化了
写过自定义播放器的都踩过这个坑——播放状态一变,就要写一堆 JS 事件监听器:play 事件要改图标、pause 事件要改图标、waiting 事件要转圈、muted 变化要切图标。每次加个状态,都要去找对应的事件、防止重复绑定、小心内存泄漏。
七个状态,一行选择器
/* 播放中:进度条高亮、绿色播放键 */
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 来回同步了。
评论区
登录后可评论。