一行 CSS 新特性干掉 20 行 JavaScript ?

Hello 大家好😁。

如果你在 2026 年,还在用 JavaScript 监听滚动事件来实现视差动画,还在用 ResizeObserver 来做组件级响应式,还在引入 Popper.js 来定位一个小小的气泡提示框......

那你欠浏览器一句道歉😀。

过去两年,CSS 在底层经历了一场极其猛烈的能力跃迁。大量曾经必须依赖 JavaScript 才能实现的交互和布局逻辑,现在只需要一行 CSS 声明就能搞定。而且由于这些能力直接运行在浏览器的渲染引擎底层,它们的性能表现,是任何 JavaScript 方案都无法比拟的。

以下是我整理出来的 6 个在 2026 年已经获得主流浏览器全面支持,但依然被国内大量前端严重低估的纯 CSS 新特性👇。


:has() 选择器 ------ 让父元素感知子元素

CSS 长达 20 多年的历史里,有一个让所有前端痛彻心扉的缺陷:你永远无法根据子元素的状态,去反向控制父元素的样式。

比如一个极其常见的表单场景:当输入框获得焦点时,让它的外层容器高亮。过去你必须用 JavaScript 监听 focusblur 事件,手动给父元素切换 class

JavaScript 手动监听

javascript 复制代码
// 为了让父容器感知子元素的焦点状态,被迫写一坨事件监听🤷‍♂️
const input = document.querySelector('.form-input');
const wrapper = document.querySelector('.form-wrapper');

input.addEventListener('focus', () => {
  wrapper.classList.add('is-focused');
});


input.addEventListener('blur', () => {
  wrapper.classList.remove('is-focused');
});
css 复制代码
.form-wrapper.is-focused {
  border-color: #3b82f6;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
}

现在是一行 CSS 直接秒杀⚡

css 复制代码
/* 父元素直接感知内部 input 的焦点状态,零 JavaScript */
.form-wrapper:has(input:focus) {
  border-color: #3b82f6;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
}

:has() 选择器的威力远不止如此。你可以用 .card:has(img) 来区分有图和无图的卡片布局;用 form:has(:invalid) 来在表单存在校验错误时,直接禁用提交按钮的视觉样式;甚至用 body:has(.modal-open) 来在弹窗打开时锁定背景滚动。

这一个选择器,干掉了过去无数的状态切换 JavaScript


@container 容器查询

传统的 @media 媒体查询,响应的是整个视口的宽度。但在组件化开发的时代,同一个卡片组件可能被放在宽度 300px 的侧边栏里,也可能被放在宽度 800px 的主内容区里。视口宽度根本不能反映组件自身的实际空间。

过去为了实现组件级响应式,你不得不引入 ResizeObserver,写一大段 JavaScript 去监听容器尺寸变化,然后手动切换布局。

ResizeObserver 的手动监听

javascript 复制代码
// 为了让卡片组件根据自身容器宽度切换布局,被迫引入 JS 观察者
const observer = new ResizeObserver(entries => {
  for (const entry of entries) {
    const width = entry.contentRect.width;
    if (width < 400) {
      entry.target.classList.add('compact');
      entry.target.classList.remove('wide');
    } else {
      entry.target.classList.add('wide');
      entry.target.classList.remove('compact');
    }
  }
});

observer.observe(document.querySelector('.card-container'));

纯 CSS 容器查询

css 复制代码
/* 声明容器 */
.card-container {
  container-type: inline-size;
}

/* 组件根据自身容器的宽度自动切换布局,零 JavaScript */
.card {
  display: grid;
  grid-template-columns: 1fr;
}

@container (min-width: 400px) {
  .card {
    grid-template-columns: 200px 1fr;
  }
}

组件不再需要关心自己被放在了页面的哪个位置,它会根据自身的物理空间自动适配布局。这是 CSS 有史以来最接近智能组件的能力跃迁。


滚动驱动动画 ------ 彻底干掉 scroll 事件监听

过去,凡是和滚动相关的动画效果(进度条、视差、淡入淡出),你都必须监听 scroll 事件。而 scroll 事件是浏览器中触发频率最高的事件之一,处理不好极其容易导致主线程卡顿和掉帧。

JavaScript 监听滚动

javascript 复制代码
// 为了实现一个顶部阅读进度条,被迫疯狂监听 scroll 事件
window.addEventListener('scroll', () => {
  const scrollTop = document.documentElement.scrollTop;
  const scrollHeight = document.documentElement.scrollHeight - window.innerHeight;
  const progress = (scrollTop / scrollHeight) * 100;
  document.querySelector('.progress-bar').style.width = `${progress}%`;
});

这段代码在用户快速滚动时,每秒会触发几十次甚至上百次回调,直接榨干主线程的算力。

现在是纯 CSS 滚动驱动动画

css 复制代码
/* 顶部阅读进度条,完全由浏览器渲染引擎在合成层驱动,零 JS 零卡顿 */
.progress-bar {
  position: fixed;
  top: 0;
  left: 0;
  height: 3px;
  background: #3b82f6;
  width: 0%;

  /* 绑定到页面的滚动进度,浏览器底层自动计算 */
  animation: grow-progress linear;
  animation-timeline: scroll();
}

@keyframes grow-progress {
  from { width: 0%; }
  to { width: 100%; }
}

整个动画被完全交给了浏览器底层的合成线程(Compositor Thread) 去驱动,根本不会占用主线程的一丝算力。这种性能差距,不是优化几行 JavaScript 能弥补的,是架构层面的降维打击。


@starting-style ------ 原生实现 display: none 的入场动画

这个特性解决了一个困扰前端十几年的深水区痛点:你无法对 display: nonedisplay: block 的切换做过渡动画。

过去,为了实现一个弹窗的淡入效果,你要么引入 Framer Motion 这种动画库,要么用 JavaScript 先把元素设为 display: block,然后在下一帧再切换 opacity,极其丑陋。

JavaScript 双帧 Hack

javascript 复制代码
// 为了让弹窗从 display:none 淡入,被迫写出这种恶心的双帧 hack
function showModal(el) {
  el.style.display = 'block';
  // 强制浏览器重排,否则 transition 不会触发
  el.offsetHeight; // 这行看起来毫无意义,但删掉动画就失效
  el.classList.add('visible');
}

纯 CSS 原生入场动画

css 复制代码
.modal {
  display: none;
  opacity: 0;
  transition: opacity 0.3s ease, display 0.3s ease allow-discrete;
}

.modal.open {
  display: block;
  opacity: 1;

  /* 告诉浏览器:从 display:none 切换过来时,opacity 从 0 开始 */
  @starting-style {
    opacity: 0;
  }
}

浏览器原生支持了离散属性(如 display)的过渡动画。不需要任何 JavaScript,不需要任何动画库,一个弹窗的淡入淡出就被几行 CSS 完美解决了🤷‍♂️。


field-sizing: content------自动撑高的 textarea

过去,为了让一个多行输入框根据内容自动调整高度(而不是固定一个死板的高度),你需要用 JavaScript 去监听 input 事件,动态计算 scrollHeight 并赋值给 style.height

JavaScript 手动撑高

javascript 复制代码
const textarea = document.querySelector('textarea');
textarea.addEventListener('input', () => {
  textarea.style.height = 'auto'; // 先重置,否则只会越来越高
  textarea.style.height = textarea.scrollHeight + 'px';
});

现在是一行 CSS

css 复制代码
textarea {
  field-sizing: content; /* 浏览器根据内容自动撑高,零 JavaScript */
}

一行 CSS,干掉了一个在前端圈流传了十几年的经典 JavaScript Hack 🖐️。


CSS Anchor Positioning ------ 原生的气泡定位

过去,只要你需要实现一个 点击按钮弹出气泡提示框,且气泡自动贴在按钮旁边 的效果,你几乎必须引入 Popper.jsFloating UI 这种定位库。因为手动计算位置、处理边界溢出翻转,这套逻辑的复杂度极高。

用 CSS 原生锚点定位

css 复制代码
/* 声明锚点 */
.trigger-button {
  anchor-name: --my-anchor;
}

/* 气泡自动定位到锚点下方,溢出时自动翻转 */
.tooltip {
  position: fixed;
  position-anchor: --my-anchor;
  top: anchor(bottom);
  left: anchor(center);

  /* 原生的溢出翻转逻辑,浏览器自动处理 */
  position-try-fallbacks: flip-block, flip-inline;
}

不需要任何 JavaScript 库,不需要计算 getBoundingClientRect,不需要处理滚动偏移。浏览器的渲染引擎直接在布局阶段帮你搞定一切,包括边界检测和自动翻转。这意味着你可以从项目的依赖里删掉一整个定位库🙌。


要知道什么时候不写 JavaScript

CSS 的这一轮能力爆发,本质上是浏览器厂商在把越来越多的交互决策权JavaScript 主线程,下沉到了渲染引擎的底层。

这不仅仅是少写几行代码的问题。每一行被 CSS 替换掉的 JavaScript,都意味着主线程少了一份负担,页面的渲染帧率高了一个台阶,用户的交互响应快了几毫秒。

今天写到这里,喜欢点赞收藏🙏

相关推荐
风月说与山鬼1 小时前
JS闭包详解
开发语言·javascript
JarvanMo1 小时前
AI 写代码暴增 161 倍,移动开发有没有变得更差?
前端
梦曦i1 小时前
RouterLink v2.5.0:H5端原生能力全面回归
前端·uni-app
小林ixn2 小时前
React + Zustand + JWT:从零实现登录鉴权与请求拦截
前端·react.js·前端框架
葡萄城技术团队2 小时前
下拉多选类型单元格:在 SpreadJS 里接入 xm\-select
前端
粥里有勺糖2 小时前
视野修炼-技术周刊第130期 | 光影效果
前端·javascript·github
newerp2 小时前
Redis 操作与缓存策略
后端·程序员·go
newerp2 小时前
GORM ORM 基础
后端·程序员·go
小岛前端2 小时前
AI Skills 已经封神,但新的问题却越来越严重!
前端·后端·github