你以为 的打开状态只能用 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() 组合时要注意。

三个下一步

  1. 现在就把 [open] 替换成 :open:给现有的 <details> 组件做这个替换,立刻解锁对 <dialog><select> 的样式支持

  2. 弹窗滚动锁定换成纯 CSS:把 JS 里的 body.overflow = hidden 换成 html:has(dialog:open) { overflow: hidden; },一行不用 JS

  3. 等全面支持后扩展到联动场景:下拉展开时父容器变色、颜色选择器打开时加亮边框——这些以前靠 JS 状态同步的场景,等浏览器全面支持后都可以用 :has(:open) 重写

评论区

0 条评论

登录后可评论。