配了三年下拉菜单,今天才发现 CSS 自己会认「开着」这个状态了——:open 把这件事彻底原生化了

以前配下拉菜单的展开态,每次都要写两套样式然后靠 JS 切——今天 CSS 自己会认「开着」这个状态了,:open 把这件事彻底原生化了。

<details> 展开时什么样式,<select> 点开时什么样式,dialog 打开时什么样式——以前全要靠classList.toggle或者[open]属性选出来再加 class,CSS 本身只管得了「隐藏」,管不了「当前是开着还是关着」这个语义状态。

这就是 :open 要解决的问题。

一行替代三年的 JS

:open是 CSS Selectors Level 4 里的伪类,专门选「当前处于打开状态」的元素。支持的元素有三个:detailsselect(下拉选择器)、input[type="date"]这类带 picker 的输入元素。

/* 以前:靠 JS 加 class */
details.is-open .content { ... }
select.is-open { ... }

/* 现在:浏览器自己认状态 */
details:open .content { ... }
select:open { ... }

浏览器自己维护打开/关闭状态,CSS 直接读这个状态,不用再绕 JS。

三种场景直接跑

场景一:details 展开样式

details {
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  overflow: hidden;
  transition: all 0.3s;
}

details:open {
  border-color: #3b82f6;
  box-shadow: 0 4px 12px rgb(59 130 246 / 0.15);
}

details:open summary {
  color: #3b82f6;
  font-weight: 600;
}

不用 JS 改 class,details 的开闭状态 CSS 自己读得到。展开时边框变蓝,加阴影,summary 文字变色,全是纯 CSS。

场景二:select 展开换箭头

select {
  appearance: none;
  background: white url("data:image/svg+xml,...") no-repeat right 12px center / 16px;
  padding: 10px 40px 10px 14px;
  border: 1px solid #d1d5db;
  border-radius: 6px;
}

select:open {
  border-color: #6366f1;
  background-color: #f5f3ff;
  background-image: url("data:image/svg+xml,...");
  outline: 2px solid rgb(99 102 241 / 0.2);
  outline-offset: 1px;
}

点开时背景变紫,箭头朝上,蓝色边框 outline。旧写法要监听focus事件改 class,现在直接:open一步到位。

场景三:父容器跟着子元素状态变

这是最实用的一类——子元素打开了,父容器也要跟着变。以前靠 JS 读子元素状态改父 class,现在靠:has()组合:open

.select-wrapper:has(select:open) {
  border-color: #6366f1;
  box-shadow: 0 0 0 3px rgb(99 102 241 / 0.15);
}

这个组合直接说「当 wrapper 里的 select 处于打开状态时,wrapper 本身也变样式」。没有 JS,没有 class 切换,就一行选择器。

这个思路是更大的趋势

:open不是孤立的案例。它是 CSS 近年来「把状态判断收归浏览器」这条主线的其中一环:

  • :popover-open——popover 显示时
  • :modal——dialog 以 modal 方式打开时
  • @position-try——弹层被屏幕边缘截断时自动换向
  • scroll-state()——滚动到了哪一段

这些以前全要靠 JS 监听+计算+改 class,现在浏览器自己算,CSS 读原生状态就行。前端写的 JS 少了,出 bug 的机会也少了。

浏览器支持与落地方案

主流浏览器全部支持(Chrome 113+、Safari 17.5+、Firefox 121+),Baseline 2026 已标注为「广泛可用」,生产环境直接用。

如果要同时适配开和关两种状态,用:not(:open)

select:not(:open) { /* 默认态 */ }
select:open { /* 展开态 */ }

下一步可以做的

把手里一个用 JS 写了下拉/折叠态的组件拿出来,把classList.add/remove的逻辑删掉,换成:open:has()。先跑通,再看能不能把 JS 完全卸掉——有些项目做到最后,状态管理的 JS 全部删光了,就剩 HTML 和 CSS。

评论区

0 条评论

登录后可评论。