2026 CSS 接管了这些 JS 的活:6 个 Baseline 特性的落地取舍

摘要: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:删除"打开状态类名"的镜像

很多组件同时维护两份状态:

  1. 浏览器里的元素已经打开;
  2. 框架里再保存一个 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"重构。更稳妥的落地方式是:

  1. 新需求优先评估原生能力,不为删代码而改旧代码;
  2. 只替换有明确回退、能独立验证的 UI 胶水逻辑;
  3. 在组件库封装兼容边界,不让业务重复判断;
  4. 用浏览器支持数据和项目用户数据决定上线时间;
  5. 在代码评审中说明删除了哪类状态,以及降级行为是什么。

一个可复用的工程判断模型

面对下一个"以前要写 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 定位、还是别的什么?


参考资料

相关推荐
全栈项目管理程序猿1 小时前
ArcGIS JS 基础教程(35):体元系列 - VoxelLayer 时间维度动画
前端·javascript
小poop3 小时前
从“雷峰塔”到“对象”:JavaScript基础语法入门指南
前端·javascript
chunyublog4 小时前
HDFS 上传文件名空格问题排障教程
前端·javascript·npm
FRing036 小时前
JavaScript入门到精通二十九(内置构造函数(Object、Array))
开发语言·前端·javascript
天才熊猫君6 小时前
前端队列,我是怎么从 `push + shift` 换成游标的
前端·javascript
Qinging6 小时前
var_let_const
javascript
Maxkim6 小时前
私钥就是数字 4:扒一扒这场把一家 CA 直接送走的事故
javascript·架构
gCode Teacher 格码致知6 小时前
前端教学8:提高 CSS 选择器的优先级,有哪些方法?
前端·css·html5
u0111026756 小时前
Canvas 导出 PNG 时 quality 参数为什么不起作用
前端·javascript·vue.js