你以为 的打开状态只能用 JS 监听?今天 Safari 用一个伪类把这件事彻底原生化了
写过手风琴组件或弹窗的人大概都有这个经历:想知道一个 <details> 或 <dialog> 是不是打开的,要么给 JS 加一个 class 变量,要么写一个 MutationObserver 盯着 open 属性变化。Safari 26.5(2026-05)给 CSS 加了一个 :open 伪类,让这件事变成了纯 CSS 的事。
:open 是什么
CSS :open 伪类,匹配所有处于”打开”语义状态的元素:
<details>— 展开时匹配<dialog>—show()或showModal()打开时匹配<select>— 下拉展开时匹配<input type="date|password|color|...">— picker 显示时匹配
这是第一次有一个伪类能同时覆盖这四类元素的打开状态。在这之前,[open] 属性选择器只能对付 <details>,<dialog> 和 <select> 完全不在它的服务范围内。
旧写法 vs 新写法
以前给 <details> 的打开状态加样式,代码大概长这样:
/* 只对 details 生效,dialog 和 select 完全不管 */
details[open] > summary {
background: #f0f0f0;
}
给 <dialog> 写样式更麻烦,需要 JS 配合:
dialog.addEventListener("toggle", () => {
document.body.classList.toggle("dialog-open", dialog.open);
});
现在一个 :open 全部搞定:
details:open > summary {
background: #f0f0f0;
}
dialog:open {
border: 2px solid blue;
}
select:open {
border: 1px solid skyblue;
}
这就是真正的语义化样式:不需要知道它是 open 属性还是 JS 控制的 class,只要它”打开着”,CSS 就认得出来。
真正的杀招::has(:open) 父级样式
这是比”省一个 class”更值得讲的变化。
以前想让弹窗打开时锁定页面滚动,只能靠 JS:
dialog.addEventListener("toggle", () => {
document.body.style.overflow = dialog.open ? "hidden" : "";
});
现在一行 CSS:
html:has(dialog:open) {
overflow: hidden;
}
同理,下拉展开时高亮对应的 label:
label:has(select:open) {
color: var(--accent-color);
}
:has(:open) 把”子元素状态驱动父元素样式”这件事从 JS 彻底接了过来。以前要写一个 MutationObserver 盯着 open 属性,现在直接问 CSS:这个元素是不是打开着?浏览器原生回答你。
浏览器支持与渐进增强
Safari 26.5(2026-05 稳定版)首发支持 :open,其他浏览器跟进中。渐进增强写法:
/* 不支持的浏览器跳过这条规则,组件功能不受影响 */
details:open > summary {
background: #f0f0f0;
}
/* 也可以用 @supports 做显式检测 */
@supports selector(:open) {
details:open > summary {
background: #f0f0f0;
}
}
需要注意的是::open 追踪的是语义状态,不是视觉可见性。一个 <details open> 即使里面有 display: none,在语义上仍然是”打开的”,:open 也会匹配。这个边界行为和 :has() 组合时要注意。
三个下一步
-
现在就把
[open]替换成:open:给现有的<details>组件做这个替换,立刻解锁对<dialog>和<select>的样式支持 -
弹窗滚动锁定换成纯 CSS:把 JS 里的
body.overflow = hidden换成html:has(dialog:open) { overflow: hidden; },一行不用 JS -
等全面支持后扩展到联动场景:下拉展开时父容器变色、颜色选择器打开时加亮边框——这些以前靠 JS 状态同步的场景,等浏览器全面支持后都可以用
:has(:open)重写
评论区
登录后可评论。