
Hello 大家好😁。
如果你在 2026 年,还在用 JavaScript 监听滚动事件来实现视差动画,还在用 ResizeObserver 来做组件级响应式,还在引入 Popper.js 来定位一个小小的气泡提示框......
那你欠浏览器一句道歉😀。
过去两年,CSS 在底层经历了一场极其猛烈的能力跃迁。大量曾经必须依赖 JavaScript 才能实现的交互和布局逻辑,现在只需要一行 CSS 声明就能搞定。而且由于这些能力直接运行在浏览器的渲染引擎底层,它们的性能表现,是任何 JavaScript 方案都无法比拟的。
以下是我整理出来的 6 个在 2026 年已经获得主流浏览器全面支持,但依然被国内大量前端严重低估的纯 CSS 新特性👇。
:has() 选择器 ------ 让父元素感知子元素
在 CSS 长达 20 多年的历史里,有一个让所有前端痛彻心扉的缺陷:你永远无法根据子元素的状态,去反向控制父元素的样式。

比如一个极其常见的表单场景:当输入框获得焦点时,让它的外层容器高亮。过去你必须用 JavaScript 监听 focus 和 blur 事件,手动给父元素切换 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: none 到 display: 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.js 或 Floating 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,都意味着主线程少了一份负担,页面的渲染帧率高了一个台阶,用户的交互响应快了几毫秒。
今天写到这里,喜欢点赞收藏🙏
