
9 个新 CSS 特性,正在悄悄干掉你的 JS
我拿一个「商品详情页」的需求,从头到尾走了一遍。原来要写三天的东西,现在两小时。
先说个事。
上周组里来了个需求,特别常规:电商商品详情页改版。产品经理列了个清单,你肯定见过——
- 顶部图片能左右滑,底下要有小圆点
- 页面往下滚,顶部导航要吸住,吸住之后加个阴影
- 右侧目录跟着滚动高亮当前区块
- 选支付方式的下拉框,要带图标和说明文字
- 商家名字 hover 上去出个店铺卡片
- 点「立即购买」弹确认框,点旁边空白能关掉
- 订单状态不同,卡片边框颜色不一样
- 推荐商品列表进场时依次淡入
排期给了三天。放在去年,这三天一点不宽裕——光轮播图我就得写 200 多行。
需求最后当然还是按老办法交的(原因下面会说)。但交完之后我不太甘心,周末拿这份需求当靶子,用 CSS Wrapped 2025 里的新特性在本地重做了一遍。
结果是:上面八件事,七件半没写一行 JS。
这篇就是那一遍的复盘。我按做需求的顺序讲,每个特性先说人话、再上代码,每一节末尾都标了真实的兼容情况和降级方案——因为这才是决定你明天能不能用上的关键。
开始前提醒三句:
- 这批东西很新。只有一个已经是 Baseline(三大浏览器全支持),其余大部分只在 Chromium 系(Chrome / Edge)里能用。我在每一节开头都标了具体版本号。
- 里面有四个(
command、commandfor、closedby、interestfor)严格说是 HTML 属性不是 CSS,但它们跟这批一起发的、解决同一类问题,就一起讲了。 - 上线前一定去 Can I Use 或 MDN 再查一遍——下面的版本号是我 2026 年 7 月核对的,这批特性推进得很快。
先看一眼全景——左边是你熟悉的活儿,右边是现在谁来干:
code 以前你写的 JS 现在归谁管 兼容情况
───────────── ────────── ────────
┌─ 点按钮弹确认框 ──────→ command/commandfor ✅ 三大浏览器
│
├─ 点空白处关掉弹窗 ────→ closedby 🟡 Chrome+FF
│
├─ hover 出店铺卡片 ───→ interestfor 🟡 Chrome 142
│
├─ 自己撸下拉框 ───────→ appearance:base-select 🟡 Chrome 135
│
├─ 轮播箭头 + 小圆点 ───→ ::scroll-button 等 🟡 Chrome 135
│
├─ 目录跟着滚动高亮 ────→ scroll-target-group 🟡 Chrome 140
│
├─ 判断导航吸顶没有 ────→ scroll-state 查询 🟡 Chrome 133
│
├─ 列表依次淡入 ───────→ sibling-index() 🟡 Chrome+Safari
│
└─ 按状态换卡片颜色 ───→ if() + attr() 🟡 Chrome 137/133(🟡 = 目前主要是 Chromium 系,Firefox / Safari 还没跟上,需要降级方案。具体到每一个我下面都会说。)
行,开工。
第一件事:点「立即购买」弹确认框
需求最简单的一条,先干掉。
以前怎么写? 按钮上挂个 onclick,里面拿到弹窗元素,调 showModal():
html<button onclick="document.querySelector('#confirm').showModal()">
立即购买
</button>
<dialog id="confirm">确认下单?</dialog>就为了「点一下弹个框」,你得写 JS。
现在怎么写? 两个属性,一行 JS 都不用:
html<button commandfor="confirm" command="show-modal">立即购买</button>
<dialog id="confirm">确认下单?</dialog>看懂了吗?commandfor 就是指一下我要操作谁,写目标元素的 id——用法和 <label for> 一模一样。command 就是说一下我要干啥。
command 能填的值,其实就是你早就会的那几个 JS 方法,换成了字符串:
你写 command="..." |
相当于以前的 |
|---|---|
show-modal |
dialog.showModal() |
close |
dialog.close() |
show-popover |
el.showPopover() |
hide-popover |
el.hidePopover() |
toggle-popover |
el.togglePopover() |
兼容性:整篇文章里唯一一个可以放心用的。 Chrome 135、Firefox 144、Safari 26.2 依次跟进,2025 年 12 月正式进入 Baseline「新近可用」。
(顺带解释下 Baseline:「新近可用」意思是三大浏览器的最新版都支持了;要等三年后才会变成「广泛可用」。所以如果你的用户里还有一批老设备,该做的降级还得做——不过这个特性的降级很简单,把 onclick 那行留着当兜底就行。)
好处不只是少打几个字。因为「谁点谁、点了开哪个」直接写在 HTML 上,后面接手的人一眼就能看明白,不用去 JS 里翻。而且——万一 JS 加载失败了,这个按钮照样能用。
第二件事:点弹窗外面的空白,关掉它
这个体验特别常见,但每次都要多写两段代码:监听遮罩点击,再监听 Esc 键。
以前:
jsdialog.addEventListener('click', (e) => {
if (e.target === dialog) dialog.close(); // 点到了遮罩
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') dialog.close(); // 按了 Esc
});顺便说,上面这段其实有个隐藏 bug。它靠「点击目标是不是 dialog 本身」来判断你点的是不是遮罩。但如果弹窗有内边距(padding),你点在那圈内边距上——那也算 dialog 本身——弹窗就莫名其妙关了。
这种边界情况,本来就不该由我们操心。
现在:一个属性搞定。
html<dialog id="confirm" closedby="any">
正在校验库存……
</dialog>closedby 就三个值,字面意思:
any—— 点外面或者按 Esc,都能关closerequest—— 只有按 Esc 能关none—— 都不行,只能你自己写代码关(适合「支付中,别乱点」这种)
有个小知识点值得知道:不写 closedby 的时候,行为看你是怎么打开的。用 showModal() 打开的,默认就能按 Esc 关;非模态的默认哪个都不能关。所以真正新增的能力是 closedby="any" 里「点外面关掉」这一半。
两个监听器,换一个属性。
兼容性:Chrome 134+、Firefox 141+ 已支持,Safari 还没完整实现(Safari 26.2 挂出了 closedBy 属性但实现不完整,别被它骗了)。这条已经进了 Interop 2026 的名单,年内大概率补齐。
在那之前,稳妥的做法是留着旧的两段监听当兜底——它俩不冲突,浏览器支持 closedby 时你的 JS 顶多是空跑一遍。
第三件事:hover 商家名字,出个店铺卡片
这就是 Tooltip / 悬浮卡片,看着简单,写起来是最烦的之一。
以前得写多少? 鼠标移入、鼠标移出、键盘聚焦、键盘失焦——四个监听。还得加个定时器,不然鼠标从按钮移向卡片的路上,卡片会一闪就没。再给触屏用户凑一个降级方案。
写完你会发现,这一坨代码,跟业务半毛钱关系没有。
现在:
html<button interestfor="shop-card">星野家居旗舰店</button>
<div id="shop-card" popover="hint">
开店 6 年 · 好评率 98.7% · 48 小时发货
</div>interestfor 的字面意思就是「对谁感兴趣」。用户鼠标悬停、键盘聚焦、或者在触屏上长按,浏览器就认为你「表达了兴趣」,自动把它指向的那个元素显示出来;兴趣结束就收起。定时器、防闪烁、触屏适配,浏览器都替你处理了。它在 <a> 标签上也能用,不只限于按钮。
还有个细节挺关键:配合的是 popover="hint",这是新的一种 popover 类型。它不会关掉你已经打开的别的浮层。
这个特性听着抽象,但场景很实在:用户点开了一个下拉菜单,然后 hover 菜单里某一项想看提示——如果提示浮层把菜单挤没了,那就白瞎了。用 hint 就不会。
兼容性:popover="hint" 从 Chrome 133 起就有了,interestfor 在 Chrome 142 正式发布(不再需要开实验开关了,这点跟半年前的说法不一样)。Firefox 和 Safari 目前都还没有,所以生产环境用它必须配降级方案。
另外说句公道话:成熟的 Tooltip 库先别删。碰撞检测(快贴到屏幕边缘要自动翻到另一边)、可交互 tooltip(鼠标能移进去点里面的链接),这些它还接不住。
它接住的是那 80% 的「就是想显示一句提示」——而这 80%,恰恰占了你 Tooltip 代码的绝大部分。
第四件事:支付方式下拉框,要图标 + 说明文字
来到今天的重头戏之一。
我先问一句:你有多少次上组件库的 Select,或者自己用 <div> 硬撸一个下拉框,纯粹只是因为原生 <select> 长得丑、还改不动?
反正我是数不清了。
原生 <select> 一直是个「黑盒」——浏览器画的,你改不了。所以我们只能自己造:一个 div 当按钮,一个 div 当面板,再补上键盘上下键、回车选中、Esc 关闭、点外面收起、无障碍属性……造一个能用的下拉框,轻松两三百行。
现在,一行 CSS 就能把这个黑盒打开:
cssselect,
::picker(select) {
appearance: base-select;
}appearance: base-select 是一个开关。打开之后,这个 <select> 就从「浏览器画的黑盒」变成了「你能随便改的普通元素」。
然后你想怎么改就怎么改:
css/* 下拉面板 */
::picker(select) {
border: 1px solid #ddd;
border-radius: 12px;
padding: 4px;
}
/* 每一个选项 */
option {
display: flex;
gap: 0.6rem;
padding: 0.5rem;
border-radius: 8px;
}
/* 当前选中的那个 */
option:checked {
background: #eef2ff;
}顺手还解决了一个老坑:下拉面板渲染在页面最顶层,不会被祖先元素的 overflow: hidden 裁掉一半。这个坑我信你踩过。同时浏览器还是会自动判断——下面空间不够就往上翻。
更爽的在后面:打开这个开关之后,<option> 里能塞富文本了。产品要的「图标 + 名称 + 说明」,直接写:
html<select>
<button>
<selectedcontent></selectedcontent>
</button>
<option value="wx">
<span class="icon">💬</span>
<span class="name">微信支付</span>
<span class="desc">推荐,免手续费</span>
</option>
<option value="ali">
<span class="icon">💰</span>
<span class="name">支付宝</span>
<span class="desc">支持花呗分期</span>
</option>
</select>那个 <selectedcontent> 也是新的。它干的事很简单:把你选中的那个 option 的内容,原样复制一份到按钮里。所以下拉收起来之后,按钮上显示的也是「💬 微信支付」,而不是干巴巴一行字。
那按钮里我只想要图标和名称,不想要那行说明呢?藏掉就行:
cssselectedcontent .desc {
display: none;
}原生下拉框,自定义外观,常规场景不用组件库了。这个我们等了差不多十年。
兼容性:Chrome / Edge 135+(2025 年 4 月),Firefox 和 Safari 都还没有。
但这个特性的降级是全场最优雅的:不支持的浏览器直接忽略 appearance: base-select,<select> 退回成原生下拉框——样子丑一点,功能一点不缺。所以哪怕现在就写,最差的结果也就是「部分用户看到的是系统默认样式」。这个代价我觉得可以接受。
第五件事:商品图轮播,带箭头和小圆点
来了,去年让我写了 200 多行的那个。
先想清楚一件事:轮播图难的从来不是"滑动"本身。滑动早就有 scroll-snap 了,几行 CSS 就能让它一屏一屏地吸附对齐。
难的是外面那一圈控件:
- 左右箭头
- 滑到第一张,左箭头要变灰
- 底下的小圆点
- 当前是第几张,对应圆点要高亮
- 点圆点要能跳过去
- 键盘方向键要能用
- 屏幕阅读器要能念出来
这一圈,CSS 现在能直接给你生成。 结构上你只需要写一个最朴素的列表:
html<ul class="gallery">
<li><img src="p1.jpg" alt="商品主图" /></li>
<li><img src="p2.jpg" alt="细节图" /></li>
<li><img src="p3.jpg" alt="尺码表" /></li>
</ul>先是老朋友,让它能一屏一屏滑:
css.gallery {
display: flex;
gap: 1rem;
list-style: none;
padding: 0;
overflow-x: auto;
scroll-snap-type: x mandatory; /* 滑动时自动吸附对齐 */
}
.gallery > li {
flex: 0 0 100%;
scroll-snap-align: center;
}到这为止都是老东西。下面开始是新的——两行 CSS,凭空长出左右箭头:
css.gallery::scroll-button(left) {
content: "◀" / "上一张";
}
.gallery::scroll-button(right) {
content: "▶" / "下一张";
}这里有两个特别值钱的点:
第一,它生成的是真按钮,能聚焦、能用键盘操作。而且滑不动的方向,浏览器自动帮你禁用——手写轮播里最烦人的那个「第一张时左箭头要变灰」,白送。
第二,注意那个斜杠。 content: "▶" / "下一张",斜杠后面是给屏幕阅读器念的名字。视障用户听到的是「下一张」,而不是「黑色右指三角形」。这种事以前得自己补 aria-label,十次有八次会忘。
接着是小圆点。先告诉容器「在我后面生成一组标记」:
css.gallery {
scroll-marker-group: after;
}然后给每一项定义它的标记长什么样:
css.gallery > li::scroll-marker {
content: "";
width: 0.6em;
height: 0.6em;
border: 1px solid currentColor;
border-radius: 50%;
}
/* 当前正在看的那一张,圆点填实 */
.gallery > li::scroll-marker:target-current {
background: currentColor;
}:target-current 就是「现在轮到你了」的意思。点圆点能跳过去,跳到哪个哪个亮——这套联动逻辑,浏览器全包了。
补一个细节:::scroll-button() 点一下滚动的距离是可视区域的 85%,不是精确一屏。想要严格一屏一屏走,就靠上面的 scroll-snap 兜住。
当然,自动播放、图片懒加载、埋点上报,这些还得你自己写 JS。但我那 200 多行里,真正属于业务的也就这三样。
兼容性:Chrome / Edge 135+,Firefox 和 Safari 都没有。
降级同样很体面:不支持的浏览器里,::scroll-button() 和 ::scroll-marker 就是不生成,你的列表退化成一个可以手指滑、可以触控板横滚的普通列表——没有箭头和圆点,但内容一条不少。对移动端来说其实影响很小。
第六件事:右侧目录跟着滚动高亮
文档站、详情页最常见的那个效果:滚到「商品参数」,右边目录里「商品参数」就变蓝。行话叫 scroll-spy。
以前基本都这么写:
jsconst observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
const link = document.querySelector(`a[href="#${entry.target.id}"]`);
if (entry.isIntersecting) link.classList.add('active');
else link.classList.remove('active');
});
});
sections.forEach((s) => observer.observe(s));看着挺人畜无害对吧?但一屏里同时出现两个区块的时候,它就开始抽了——两个都算「进入视口」,两个都高亮。
真要写对,你得算交叉比例、得设 rootMargin、还得处理「页面滚到最底部时最后一节永远高亮不上」这个经典边界。我敢说你线上那份,多半也没处理全。
现在。 HTML 就是最普通的一组锚点链接,一点不特殊:
html<nav class="toc">
<a href="#detail">商品详情</a>
<a href="#spec">规格参数</a>
<a href="#review">用户评价</a>
</nav>
<section id="detail">…</section>
<section id="spec">…</section>
<section id="review">…</section>CSS 加两句:
css.toc {
position: sticky;
top: 0;
background: #fff;
scroll-target-group: auto; /* 关键就是这行 */
}
.toc a:target-current {
color: #2563eb;
font-weight: 600;
}scroll-target-group: auto 的意思是:「把我里面这组链接,当成一组滚动标记来管」。然后 :target-current 就会命中你当前正在看的那一节对应的那个链接。
跟上一节的小圆点是同一套机制,只不过这次标记是你自己写的链接。
顺手再补两句体验优化:
csshtml { scroll-behavior: smooth; } /* 点目录平滑滚过去 */
section { scroll-margin-top: 4rem; } /* 别被吸顶导航挡住标题 */最舒服的是:结构一点没变,就是一组最普通的 <a href="#id">。JS 挂了、CSS 没加载,它退化成一个能用的锚点导航。这才是渐进增强该有的样子。
兼容性:Chrome / Edge 140+,Firefox 和 Safari 没有。
这个是全场降级最省心的一个——不支持的浏览器里,你的目录就是一组普通锚点链接,点了照样能跳,只是不会自动高亮。丢的是锦上添花,不是功能本身。 我个人觉得这个可以现在就上。
第七件事:导航吸顶之后,加个阴影
需求原话是:「导航吸住的时候加个阴影,没吸住的时候别加」。
听着简单,但难点在于——CSS 以前不知道自己「吸住了没」。position: sticky 是浏览器内部行为,样式层面感知不到。
所以以前的解法要么是监听滚动事件加节流,要么是在导航上面放一个隐形的「哨兵」元素,用 IntersectionObserver 盯着它有没有滚出视口。都挺绕。
现在 CSS 可以直接问了。
用法借用了容器查询的语法。两步:先把元素声明成「我要被查询滚动状态」,然后就能查了。
cssheader {
position: sticky;
top: 0;
container-type: scroll-state; /* 第一步:声明 */
> nav {
transition: box-shadow 0.3s ease;
/* 第二步:查询——吸顶了就加阴影 */
@container scroll-state(stuck: top) {
box-shadow: 0 2px 8px rgb(0 0 0 / 0.15);
}
}
}能查的状态有四种,名字都很直白:
stuck—— 吸住了没(Chrome 133)snapped—— 吸附对齐了没(Chrome 133)scrollable—— 还能不能继续滚(Chrome 133)scrolled—— 刚才往哪个方向滚的(Chrome 144 才有,比前三个晚一年,用之前留意下)
最后那个 scrolled 是做「向下滚隐藏导航栏、向上滚又冒出来」那种效果用的,属于新到不能再新的那一批。
这里有一条规则必须记住:你不能给容器本身设样式,只能给它的后代设。
这就是为什么上面例子里,阴影加在 > nav 上,而不是 header 上。第一次用的人 90% 都在这卡住过,包括我。
再看个用 snapped 的:横滑推荐位里,当前这张亮,其他的压暗。这个效果以前要靠监听滚动算位置:
css.shelf {
display: flex;
gap: 1rem;
overflow-x: scroll;
scroll-snap-type: x mandatory;
> div {
container-type: scroll-state;
scroll-snap-align: center;
flex: 0 0 70%;
> * { transition: opacity 0.5s ease; }
/* 没被吸附对齐的(也就是不在中间的),压暗 */
@container not scroll-state(snapped: x) {
> * { opacity: 0.25; }
}
}
}调试小贴士:写完发现啥效果都没有的时候,别急着怀疑自己的选择器。先去控制台跑一句:
jsCSS.supports('container-type: scroll-state')返回 false,那是浏览器不支持,不是你写错了。这一句能省你半小时。
兼容性:Chrome / Edge 133+,Firefox 和 Safari 没有。 需要跨浏览器的场景,老老实实用 IntersectionObserver 加哨兵元素——这也是官方文档给的建议。
第八件事:推荐商品依次淡入
产品最爱提的效果:列表项一个接一个滑进来,不要齐刷刷一起出现。
以前怎么做? 要么在 CSS 里给每一项硬写一个下标,写十几行 :nth-child。要么更省事但更糟——用 JS 循环打内联样式:
jsdocument.querySelectorAll('.item').forEach((el, i) => {
el.style.setProperty('--index', i);
});问题是:列表一变,就得重跑一遍。分页加载、虚拟列表、筛选切换的场景下,你得到处补这行代码,漏一个地方动画就乱。
现在 CSS 自己知道「我是第几个」了。
css.item {
transition: opacity 0.25s ease, translate 0.25s ease;
/* sibling-index() 从 1 开始,减 1 让第一项不延迟 */
transition-delay: calc(0.1s * (sibling-index() - 1));
@starting-style {
opacity: 0;
translate: 1em 0;
}
}sibling-index() 直接返回「我在兄弟节点里排第几」。第一项延迟 0 秒,第二项 0.1 秒,第三项 0.2 秒——错峰效果就出来了。
(@starting-style 是入场动画的起点状态,意思是「我刚出现的那一瞬间长这样」。)
新增一项,它自己就带着对应的延迟滑进来,你什么都不用管。
还有个配套的 sibling-count(),返回兄弟节点总数。这俩一配,能解决一个真实的线上事故级 bug:
css.item {
/* 不管几项,整串动画总共就跑 0.5 秒 */
transition-delay: calc(0.5s / sibling-count() * (sibling-index() - 1));
}想想按老写法:每项延迟 0.1 秒,测试环境 5 条数据看着挺舒服,上线之后用户搜出 100 条——最后一项要等 10 秒才出现。这种「数据量一大动画就离谱」的 bug,线上不知道躺着多少。
用 sibling-count() 把总时长锁死,10 项还是 100 项,永远 0.5 秒跑完。
兼容性:这个比前面几个好一些——Chrome / Edge 138+,Safari 26.2 也支持了,只差 Firefox。
而且它天然自带兜底:不支持的浏览器里 transition-delay 那行整条失效,所有项一起淡入。效果打折,但不会坏。
第九件事:订单状态不同,卡片边框不同色
最后一个,也是「我用 JS 纯粹只为了改个样式」这件事的终极形态。
以前的套路你太熟了:读一下 data-status,判断一下,然后 classList.toggle() 换个 class。绕一大圈,就为了改个颜色。
CSS 现在能直接读 HTML 上的数据了。
先说 attr()。这个函数你可能见过,以前它只能往 content 里吐字符串,用途很窄。现在它可以带类型,用在任何属性上:
css.tag {
background: attr(data-color type(<color>), #999);
}html<div class="tag" data-color="#2563eb">进行中</div>读这行:「去拿 data-color 这个属性,按颜色来理解它,拿不到就用 #999 兜底」。
关键是它是动态的。从 JS、从 Vue、从 React,随便哪改掉那个属性,样式立刻跟着变:
jstag.dataset.color = '#16a34a'; // 样式马上变绿样式逻辑住在 CSS 里,JS 只负责改数据。这个分工,比 classList.toggle() 干净太多。
(有个刻意留的口子:attr() 不能用来拼 URL。因为属性里可能存着 token 之类的东西,让它流进网络请求太危险,所以设计上直接禁掉了。)
然后是 if()——就是 CSS 版的三元表达式:
css.layout {
display: flex;
flex-direction: column; /* 老浏览器走这行兜底 */
flex-direction: if(media(orientation: landscape): row; else: column);
}以前为了改一个属性得开一整个 @media 块,现在写在值里就行。
两个一配,订单状态的需求就这么解决了:
css.order-card {
/* 把 data-status 读成一个关键字 */
--status: attr(data-status type(<custom-ident>), unknown);
border: 3px solid;
border-color: if(
style(--status: paid): #16a34a;
style(--status: refunded): #dc2626;
else: #9ca3af
);
}html<div class="order-card" data-status="paid">已支付</div>
<div class="order-card" data-status="refunded">已退款</div>
<div class="order-card" data-status="pending">待处理</div>后端返回什么状态,前端原样吐到 data-status 上,颜色自己就对了。没有 JS 判断,没有 class 映射表。
上面这种「比对具体值」的写法最稳。至于比大小,是这批里最晚落地的一块(Chrome 142 才随「样式查询范围语法」一起发布)。
有个坑必须知道:想让 CSS 把一个值当成真数字来比较,得先用 @property 报备一下,说清楚它是什么类型。否则 CSS 只当它是一串没意义的字符,比不了大小:
css@property --progress {
syntax: "<percentage>";
inherits: false;
initial-value: 0%;
}
.progress-card {
--progress: attr(data-progress type(<percentage>), 0%);
background: #f59e0b;
background: if(style(--progress > 80%): #16a34a; else: #f59e0b);
}html<div class="progress-card" data-progress="30%">进度 30%</div>
<div class="progress-card" data-progress="90%">进度 90%</div>(能比较的类型有限制:两边必须是同一种数值类型,<length>、<number>、<percentage>、<angle>、<time> 这几类。拿颜色比大小是不行的。)
兼容性:类型化 attr() 从 Chrome 133、if() 从 Chrome 137、比大小从 Chrome 142;Firefox 和 Safari 都还在开发中。
注意我上面每段都写了两行 background——第一行是普通值,第二行才是 if()。这不是手滑,是故意的降级:不认识 if() 的浏览器会直接丢掉第二行,留下第一行的兜底色。CSS 的容错机制这时候就是你的降级方案,不用写任何判断。
方向已经很明确了:那些你靠 classList.toggle() 干的事,CSS 正在一件件接过去。
所以,JS 被开除了吗?
没有,差得远。
真正的业务逻辑、调接口、状态管理、复杂交互编排,这些还是 JS 的主场,短期内看不到替代品。
但你冷静回想一下自己每天写的 JS,有多大比例其实不是逻辑,是"胶水"?
开弹窗、关弹窗、算滚动位置、高亮导航、根据一个字段切 class——这些代码没有任何业务价值,却要占 bundle 体积、要写测试、要在无障碍上补课,还要在某个周五下午莫名其妙地坏掉。
CSS Wrapped 2025 干的事,说白了就是平台把这部分管道工程收回去了。
包更小,能坏的地方更少,无障碍默认更好。还有我最看重的一条:JS 加载失败的时候,这些东西照样能用。
但也得把话说完整。
回到开头那个需求——它最后为什么还是按老办法交的?因为公司的兼容基线要覆盖 Safari 和一部分老设备,而这九个里只有 command / commandfor 一个是三大浏览器都支持的,其余全是 Chromium 独苗。
所以现阶段的现实做法,我觉得是这样:
可以现在就用(降级代价小,退化后功能不缺):
command/commandfor——已 Baselineappearance: base-select——不支持就退回原生下拉框scroll-target-group——不支持就是不高亮,链接照样能点sibling-index()——不支持就是一起淡入if()+attr()——写两行,前一行天然兜底
可以在内部系统、Electron、企业后台先玩起来(这些场景你能锁定 Chromium):
- 轮播那套、
scroll-state查询、interestfor
先观望:
scroll-state(scrolled)(Chrome 144 才有)、if()里的比大小(Chrome 142 才有)
我给自己的判断是:这批东西现在最大的价值不是"删代码",是"知道未来两年这些代码要被删"。你现在写的每一个轮播组件、每一个 scroll-spy,心里都该有个数——它是临时的。
想看完整清单,去 CSS Wrapped 2025 翻剩下那 13 个,细节配合 MDN 查。
本文所有版本号截至 2026 年 7 月,上线前请自行复核。
📋 兼容性速查表(2026 年 7 月核对)
| 特性 | Chrome/Edge | Firefox | Safari | 不支持时会怎样 |
|---|---|---|---|---|
command / commandfor |
135+ | 144+ | 26.2+ | ✅ 已 Baseline |
<dialog closedby> |
134+ | 141+ | ❌ 未完整实现 | 点外面关不掉,留旧监听兜底 |
interestfor |
142+ | ❌ | ❌ | 提示不显示,需 JS 降级 |
popover="hint" |
133+ | ❌ | ❌ | 同上 |
appearance: base-select |
135+ | ❌ | ❌ | 退回原生下拉框,功能不缺 |
<selectedcontent> |
135+ | ❌ | ❌ | 同上 |
::scroll-button() / ::scroll-marker |
135+ | ❌ | ❌ | 无箭头和圆点,仍可手滑 |
scroll-target-group |
140+ | ❌ | ❌ | 不高亮,锚点仍可跳转 |
scroll-state() 查询 |
133+ | ❌ | ❌ | 样式不生效,用 IO 兜底 |
└ scrolled 状态 |
144+ | ❌ | ❌ | 同上 |
sibling-index() / sibling-count() |
138+ | ❌ | 26.2+ | 动画一起播,不错峰 |
类型化 attr() |
133+ | ❌ | ❌ | 走前一行兜底值 |
if() |
137+ | ❌ | ❌ | 同上 |
└ 比大小(> <) |
142+ | ❌ | ❌ | 同上 |
聊两句
如果现在让你从这 9 个里挑一个,明天就重构进项目,你选哪个?
- A:
command/commandfor——三大浏览器都支持了,零风险,删弹窗胶水代码最爽 - B:轮播图那套(
::scroll-button+::scroll-marker)——这玩意我是真写吐了 - C:
appearance: base-select——等了十年,就等它稳定 - D:
scroll-state滚动状态查询——终于能干掉那堆滚动监听 - E:我全都要,但公司要兼容的浏览器版本不允许(说的是不是你?)
评论区留下你的选项和理由。
如果这篇对你有帮助,欢迎关注公众号「前端达人」,每周更新实用前端干货。

