摘要:
field-sizing、contrast-color()、shape()、兄弟索引函数、容器样式查询和:open在 2026 年陆续进入 Baseline。它们没有取代 JavaScript,却开始接管尺寸监听、颜色判断、响应式图形、序号计算、组件变体和开关状态等 UI 胶水代码。本文通过 6 组新旧实现,讨论哪些逻辑适合交还浏览器,哪些仍然必须留在 JavaScript。
最近在 B 站前端教学视频里,经常能听到一句很抓人的话:
这个效果以前要写 JavaScript,现在一行 CSS 就够了。
这句话很快引发讨论:有人据此判断 JavaScript 又要"失业",也有人认为新语法只是 Demo 玩具,生产项目不敢用。两种判断都太快。
截至 2026 年 10 月,本文讨论的 6 个能力都已经进入 Baseline 2026 Newly available。
它们的确能删除一批 JavaScript,但删掉的不是业务逻辑,而是为了测量、派生或同步浏览器状态而存在的 UI 胶水代码。
真正值得讨论的问题不是"CSS 能不能取代 JavaScript",而是:
当浏览器已经理解某种状态时,我们还有没有必要在 JavaScript 里再维护一份副本?
先划边界:这次要删的是哪类 JavaScript
在开始讲特性之前,先把话说清楚:本文讨论的不是"CSS 取代 JS",而是把那些本来就该用 CSS 做、以前因为 CSS 做不了才被迫用 JS 的胶水代码,还给 CSS。
适合交还给 HTML/CSS 的逻辑,通常同时满足三个条件:
- 输入来自浏览器已经掌握的状态:元素尺寸、打开状态、内容长度;
- 输出只是视觉表达,不改变业务数据;
- 状态变化不需要请求、权限判断或跨组件编排。
反过来,只要涉及业务数据、不可逆副作用或复杂状态组合,JavaScript 仍然是正确位置。
也别误会------不是所有项目都该立刻跟进。
如果你的用户跑在固定旧版 WebView、政企终端或长期支持浏览器里,这些特性可以了解,但先别急着上生产。
Baseline Newly available 只代表核心浏览器当前版本支持,不代表你的用户已经升级。
1. field-sizing:删除输入框高度同步
自适应高度的评论框,是很典型的"浏览器明明知道内容高度,我们却要重新测一遍"。
👆🏻 旧方案(JS):
js
// Before:JavaScript 负责测量并回写高度
const textarea = document.querySelector("textarea");
textarea.addEventListener("input", () => {
textarea.style.height = "auto";
textarea.style.height = `${textarea.scrollHeight}px`;
});
问题不只是这五行。组件卸载要移除监听,受控输入要处理回填,SSR 后还可能闪一下;多个输入框一起出现时,还要避免把布局读取和写入反复交错。
📌 新方案(CSS):
css
/* After:浏览器根据内容计算控件尺寸 */
textarea {
field-sizing: content;
min-block-size: 3lh;
max-block-size: 10lh;
resize: none;
}
field-sizing: content 已于 2026 年 6 月进入 Baseline Newly available。内容增加时,控件会在约束范围内自动生长;达到 max-block-size 后,再回到滚动。


收益:删除事件监听和高度状态同步,不需要在组件卸载时清理监听。
边界 :它只负责布局尺寸,不负责输入校验、格式化、字数统计。CSS 能回答"应该显示多高",回答不了"这段内容能不能提交"。还要注意空输入状态------没有 min-block-size 时,单行输入框可能缩到只剩光标宽度。
工程判断:
只替换"尺寸同步",不要顺手删掉业务校验。生产代码不能只写一行属性,还要把最小、最大尺寸一起定义。
生产环境注意事项 :在 React/Vue 受控组件里用的时候,注意 SSR hydration 阶段------服务端渲染的 textarea 高度是固定的,客户端 hydration 后浏览器才开始算 field-sizing,极端情况下可能闪一下。
如果这个闪烁不能接受,在首屏的关键输入框上保留 JS 方案,非首屏再用 CSS。另外,field-sizing 对 input 的宽度生效,对 textarea 的高度生效,别搞混了。
2. contrast-color():删除黑白文字颜色判断
标签、头像占位符和用户自定义主题,经常允许背景色动态变化。
👆🏻 旧方案(JS):
js
// Before:省略颜色格式转换,只展示判断核心
function getTextColor([r, g, b]) {
const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
return luminance > 128 ? "#000" : "#fff";
}
tag.style.color = getTextColor(parseColor(tag.dataset.color));
这段裁剪代码已经暴露出真实代价:要先兼容 HEX、RGB、透明度等输入,再保证算法与设计规范一致。颜色每变化一次,JavaScript 还要把派生结果写回 DOM。
📌 新方案(CSS):
css
/* After:前景色直接从背景色派生 */
.tag {
--tag-bg: oklch(68% 0.18 250);
background: var(--tag-bg);
color: contrast-color(var(--tag-bg));
}
contrast-color() 于 2026 年 4 月进入 Baseline Newly available。它会在黑色和白色中选择对比度更高的一个,适合替代"背景变化后手动切换黑白文字"的判断。

收益:删掉颜色解析与 DOM 回写,背景色变化后不再触发脚本。
边界:只能在黑、白之间选择,不做完整的 WCAG 校验,也不替你评估字号、字重、交互状态和品牌色约束。自动选择对比度更高的颜色,不等于自动满足所有无障碍要求。
工程判断:
背景来自受控色板且目标只是黑白二选一,可以使用;背景允许用户任意输入,要在数据入口做对比度校验;品牌组件要求固定前景色,应该由 Design Token 明确配对,而不是运行时猜测。它删除的是简单的颜色分支,不是可访问性评审。
3. clip-path: shape():删除响应式图形的坐标重算
波浪形 Banner、异形卡片这类效果,过去常用 SVG path。问题是 path() 的坐标基本锁在固定数值里;元素尺寸变化后,如果还要求波峰比例不变,项目往往会监听尺寸并重新生成路径。
👆🏻 旧方案(JS):
js
// Before:尺寸变化后重新拼接 SVG path
const observer = new ResizeObserver(([entry]) => {
const { width, height } = entry.contentRect;
const waveY = height * 0.82;
banner.style.clipPath =
`path("M 0 0 L ${width} 0 L ${width} ${waveY} ` +
`C ${width * 0.65} ${height} ${width * 0.35} ${height * 0.65} 0 ${waveY} Z")`;
});
observer.observe(banner);
这类代码把视觉坐标计算放进了运行时:要创建和销毁 Observer,要处理初始尺寸,还要保证字符串路径与设计稿同步。
📌 新方案(CSS):
css
/* After:控制点使用百分比,形状随元素尺寸变化 */
.banner {
clip-path: shape(
from 0% 0%,
line to 100% 0%,
line to 100% 82%,
curve to 0% 82% with 65% 100% / 35% 65%,
close
);
}
shape() 于 2026 年 2 月进入 Baseline Newly available。与 path() 相比,它不只语法更接近 CSS,关键是横纵坐标可以用百分比表达,所以元素伸缩时不需要 JavaScript 重算路径。


收益:删除尺寸监听和路径字符串拼接,形状随容器自动伸缩。
边界:它解决响应式裁剪,不解决图形编辑、碰撞检测和数据可视化。
工程判断:
装饰性形状适合渐进增强,承载信息的图形不应只靠裁剪。不支持 shape() 时,可以保留矩形背景作为回退;如果图形本身承载数值、选区或交互语义,SVG 或 Canvas 仍然更合适。
4. sibling-index() / sibling-count():删除序号注入
让卡片按顺序错开、围成扇形,难点从来不是 transform,而是 CSS 过去不知道"我排第几个,一共有几个"。
👆🏻 旧方案(JS):
vue
<!-- Before:框架把数组信息翻译成 CSS 变量 -->
<article
v-for="(card, index) in cards"
:key="card.id"
class="card"
:style="{
'--index': index + 1,
'--count': cards.length,
}"
>
{{ card.title }}
</article>
这段代码没有业务价值,只是在把 DOM 本来就知道的兄弟顺序转交给 CSS。列表增删后,框架还要重新计算并回写每个元素的内联样式。
📌 新方案(CSS):
css
/* After:根据当前位置与中点计算扇形角度 */
.card {
--center: calc((sibling-count() + 1) / 2);
--offset: calc(sibling-index() - var(--center));
transform-origin: 50% 120%;
transform: rotate(calc(var(--offset) * 10deg));
transition-delay: calc((sibling-index() - 1) * 40ms);
}
sibling-index() 和 sibling-count() 于 2026 年 8 月进入 Baseline Newly available。前者返回当前元素在兄弟元素中的 1-based 位置,后者返回同一父元素下的元素总数,并且包含自身。

收益:删除循环中的样式注入,增删卡片后旋转角度与动画延迟由 CSS 自动重算。
边界 :函数统计的是 DOM 兄弟元素,不理解过滤后的业务数组、分页序号或虚拟列表中的全局位置。还有一个容易忽略的除零问题:如果公式使用 sibling-count() - 1 把元素铺满半圆,只有一个元素时分母为零。新函数减少了 JavaScript,不会替你消灭数学边界。
工程判断:stagger 动画、扇形排列、序号渐变这类纯视觉场景直接替换;涉及业务排序、过滤后序号、虚拟列表的场景,仍然需要 JS 注入。
5. Container Style Queries:删除组件主题透传
组件复用到不同区域时,旧方案常常一路传 tone="night":
👆🏻 旧方案(JS):
vue
<!-- Before:视觉上下文穿过组件树 -->
<Sidebar tone="night">
<UserPanel tone="night">
<UserCard tone="night" />
</UserPanel>
</Sidebar>
问题不在这一层,而在第五层。主题本来是容器上下文,中间组件却必须声明、校验并继续转发一个自己不用的 prop。
📌 新方案(CSS):
css
/* After:容器声明视觉语境,后代自行响应 */
.sidebar {
--tone: night;
}
@container style(--tone: night) {
.user-card {
color: oklch(96% 0.01 250);
background: oklch(24% 0.02 250);
}
}
它于 2026 年 5 月进入 Baseline Newly available。与尺寸容器查询不同,样式查询不要求额外声明 container-type;任意祖先都可以成为样式容器。


收益:新增一个中间组件时,不再为了主题补一层 prop 透传,组件 API 不再被纯视觉上下文污染。
边界 :跨浏览器稳定支持的查询对象主要是自定义属性。不要看到规范示例里能查询普通 CSS 声明,就立刻把它当成生产基线。
工程判断:
这个能力适合表达紧凑/宽松密度、日间/夜间表面或品牌视觉变体,不适合承载权限、会员等级、审核状态等业务语义。
把 --is-admin: true 写进 CSS,不是解耦,只是把业务状态藏到了更难追踪的位置。
props 是否应该删除,取决于状态归谁负责------只影响视觉且由容器决定,样式查询更自然;子组件的结构、行为或数据也随状态变化,props 仍然应该保留。
生产环境注意事项 :调试样式查询比调试媒体查询反直觉得多------DevTools 里你看不到"这个元素匹配了哪个 container",得自己一层一层往上找祖先容器的 CSS 变量。建议团队约定:凡是用 @container style() 的地方,在组件文档里写清楚"这个组件会响应哪个容器变量",否则三个月后没人记得住。
6. :open:删除"打开状态类名"的镜像
很多组件同时维护两份状态:
- 浏览器里的元素已经打开;
- 框架里再保存一个
isOpen,只为了挂.is-open类名。
👆🏻 旧方案(JS):
vue
<!-- Before:框架状态只为同步一个样式类 -->
<script setup lang="ts">
import { ref } from "vue";
const isOpen = ref(false);
const onToggle = (event: Event) => {
isOpen.value = (event.currentTarget as HTMLDetailsElement).open;
};
</script>
<template>
<details :class="{ 'is-open': isOpen }" @toggle="onToggle">
<summary>运行详情</summary>
<div>任务日志</div>
</details>
</template>
这里的 isOpen 不参与业务判断,只是在复制 <details> 已经拥有的状态。两个状态源一旦不同步,图标和实际展开状态就会相互矛盾。
📌 新方案(CSS):
css
/* After:样式直接读取原生打开状态 */
details:open > summary .chevron {
rotate: 180deg;
}
dialog:open {
opacity: 1;
scale: 1;
}
select:open {
border-color: var(--color-focus);
}
:open 于 2026 年 5 月进入 Baseline Newly available。它看似只是把 [open] 写成伪类,实际价值是统一语义:样式关心"当前是否打开",而不是某个组件恰好有没有某个类。它统一匹配 details、dialog、select 下拉等所有有"打开/收起"语义的元素。

收益:删除框架状态、事件回写和类名同步,不再维护状态镜像。
边界 ::open 不负责打开元素,只读取浏览器已经存在的状态。dialog 是否打开、关闭后如何提交结果,仍可能需要 JavaScript;Popover 也要使用 :popover-open,不是 :open。当打开动作还要触发请求、埋点、权限判断或跨组件联动时,不要为了"纯 CSS"放弃事件。删除状态镜像,不等于删除所有状态逻辑。
选型速查表:什么时候换 CSS,什么时候保留 JS
| 场景 | 优先 CSS | 保留 JS |
|---|---|---|
| 表单控件尺寸跟随内容 | ✅ field-sizing | 复杂多约束联动布局、输入校验 |
| 标签/色块文字颜色适配 | ✅ contrast-color() | 品牌定制色彩算法、用户色板校验 |
| 响应式异形裁切/波浪 | ✅ clip-path: shape() | 数据可视化、可编辑图形 |
| 列表 stagger 动画、扇形排列 | ✅ sibling-index()/count() | 业务排序、虚拟列表全局序号 |
| 组件主题/密度变体 | ✅ @container style() | 权限、会员等级等业务状态 |
| 展开/收起状态样式 | ✅ :open | 打开后拉数据、埋点、跨组件联动 |
核心判断口诀:只改外观、没有副作用、浏览器已是状态源 → 交给 CSS;需要读数据、发请求、存状态、上报、联动业务 → 交给 JS。
把 6 个特性放在一起,CSS 到底接管了什么
回头看这 6 个例子,被删除的 JavaScript 大致分成三类:
第一类:测量与几何重算。 读取 scrollHeight、监听尺寸,再把高度或路径写回内联样式。field-sizing 与 shape() 让浏览器直接完成闭环。
第二类:派生。 从背景色推导文字色,从兄弟位置推导排列角度,从容器主题推导组件外观。contrast-color()、兄弟索引函数和样式查询把视觉派生留在样式层。
第三类:状态镜像。 元素已经打开,框架却再维护一个 isOpen,只为添加样式类。:open 让 CSS 直接读取原生状态。
这才是"CSS 接管 JavaScript"的准确含义:浏览器开始原生理解更多 UI 状态,应用就没必要再模拟一遍。
它没有吃掉数据流、业务规则和副作用。
客观评估:能确认的收益,和暂时不能下的结论
只根据这些实现对照,我们能确认三件事:
- 状态源减少了:不再同时维护 DOM 原生状态、框架状态和样式类;
- 生命周期代码减少了:一部分尺寸监听、事件注册、清理和 DOM 回写被浏览器接管;
- 组件 API 更聚焦:纯视觉上下文不必穿过每层业务组件。
但没有线上采样、性能剖析和维护工时记录,就不能进一步宣称"页面性能提升 30%"或"开发效率翻倍"。少几行 JavaScript 也不必然带来可感知性能收益,真正稳定的收益是少了一条可能失同步的状态链。
遗留风险同样明确:
- Baseline Newly available 与项目真实浏览器分布之间存在时间差;
- 原生状态与新 CSS 语法可能和现有设计系统、构建链约定不一致;
- 状态从框架层移到 DOM/CSS 后,团队需要更新调试方法;
- 新旧方案并存期间,组件库要承担两套实现的测试成本。
因此,这不是一次追求"代码行数下降"的改造,而是一次状态所有权调整。只有原生能力能接住状态、降级不破坏核心功能时,删除 JavaScript 才是净收益。
Baseline 不等于"今天可以无脑上线"
Baseline Newly available 表示该能力已经进入核心浏览器的当前版本,不表示你的用户已经完成升级。
例如企业内网、WebView、政企终端和长期支持版本,浏览器分布可能比公开网站慢一到两年。团队采用前至少要回答三个问题:
- 用户浏览器数据是否覆盖目标版本?
- 不支持时是样式降级,还是功能不可用?
- 构建链是否会错误转换、删除或误报新语法?
适合渐进增强的写法,可以显式提供回退:
css
.tag {
color: white;
}
@supports (color: contrast-color(red)) {
.tag {
color: contrast-color(var(--tag-bg));
}
}
如果旧浏览器只是得到固定高度、固定文字色或没有动画,通常可以接受;如果用户无法提交表单或打开关键操作,就必须提供等价方案。
一个真实的反面案例
为了赶一波"纯 CSS 表单校验"的热潮,用 :has() 把整个表单的校验状态全写在了 CSS 里:
css
/* 看起来很优雅,但维护起来是噩梦 */
.form:has(input[name="email"]:invalid) .submit-btn {
opacity: 0.5;
pointer-events: none;
}
.form:has(input[name="password"]:invalid) .password-hint {
color: #e53e3e;
}
.form:has(input[name="confirm-password"]:not(:placeholder-shown):not(:valid)) .confirm-hint {
display: block;
}
/* ......一共叠了 17 层选择器 */
问题出在哪?
第一,校验规则变了(比如密码复杂度要求从6位改成8位),你得改 HTML 里的 pattern 属性,同时记得改 CSS 里对应的提示逻辑------两个地方要同步,忘了一个就出线上 bug。
第二,产品说"校验失败时弹一个 toast 提示用户具体原因"。你用 CSS 写的状态,JS 怎么知道什么时候该弹 toast?又得写一套 JS 监听来和 CSS 状态对齐------等于把刚才删掉的状态镜像又加回来了。
第三,新人接手的时候,看到 .form:has(input:invalid) 第一反应是"这是什么写法",不敢改也不敢删,最后只能绕着走。
这个案例说明一个道理:CSS 能做到,不代表 CSS 该做。
表单校验有明确的业务规则、用户反馈、错误提示文案,这些都是逻辑,不是样式。硬塞给 CSS,只是把代码从一个文件搬到了另一个文件,维护成本一点没降。
CSS 了解成本,也是一种技术债
新能力减少了运行时代码,却增加了团队认知面。
过去排查这些效果,大家会搜索 ResizeObserver、数组索引或 isOpen;现在逻辑可能藏在 shape() 控制点、兄弟计数函数、样式查询和 :open 中。单个实现更短,不代表系统天然更容易理解。
这类技术债主要来自四个地方:
- 发现成本:成员不知道平台已经提供能力,继续复制旧方案;
- 兼容成本:只看"Chrome 能用",没看 Baseline 和真实用户版本;
- 调试成本:状态分散在 DOM 语义、伪类和级联层中;
- 规范成本:同一场景一半组件用原生能力,一半继续维护自定义实现。
因此,不建议发起一次"全量去 JavaScript"重构。更稳妥的落地方式是:
- 新需求优先评估原生能力,不为删代码而改旧代码;
- 只替换有明确回退、能独立验证的 UI 胶水逻辑;
- 在组件库封装兼容边界,不让业务重复判断;
- 用浏览器支持数据和项目用户数据决定上线时间;
- 在代码评审中说明删除了哪类状态,以及降级行为是什么。
一个可复用的工程判断模型
面对下一个"以前要写 JS"的 CSS 新特性,可以按这条链路判断:
text
浏览器是否已掌握状态
→ 是否只影响视觉或标准交互
→ 是否存在业务副作用
→ Baseline 与用户版本是否匹配
→ 降级后核心功能是否可用
→ 团队能否维护和调试
前两项为"是"、第三项为"否",通常值得交给 HTML/CSS。
一旦涉及业务副作用、跨组件协调或关键功能降级,就让 JavaScript 留在场上。不要为了追逐"纯 CSS"而把清晰的状态机改写成选择器谜题。
总结
2026 年 CSS 最重要的变化,不是又多了几个属性,而是它开始接管过去必须由 JavaScript 模拟的浏览器状态:
field-sizing接管表单控件的内容尺寸;contrast-color()接管受控场景下的黑白对比色选择;shape()接管随元素伸缩的裁剪路径;sibling-index()与sibling-count()接管兄弟位置和总数计算;- Container Style Queries 接管视觉上下文派生;
:open接管原生打开状态的样式表达;
JavaScript 没有被取代。它只是终于不用继续承担那些本该属于浏览器的工作。
对前端工程师来说,能力要求反而更高了:既要知道平台新增了什么,也要判断它的兼容边界、语义边界和组织成本。会写更少的代码只是表面,真正的价值是减少重复状态源。
你们项目里现在还有多少"其实可以用 CSS 干掉"的 JS 胶水代码?
评论区报个数------是 textarea 自适应高度、tooltip 定位、还是别的什么?