每隔几年,浏览器都会悄悄推出一波新能力,然后让你代码库里一大块旧逻辑突然显得多余。
position: sticky 干掉了一堆 jQuery scroll plugins(滚动插件)。
Flexbox(弹性布局)淘汰了不少旧式 grid frameworks(网格框架)。
而现在,2025 到 2026 年这波变化,不是一个新特性。
而是二十多个新能力一起到来。
它们瞄准的,正是你每天都在写的 JavaScript。
免费阅读链接给非会员读者。
Tooltips(提示气泡)。
Carousels(轮播图)。
Modals(弹窗)。
Conditional styles(条件样式)。
Reusable logic(可复用逻辑)。
这些东西,现在要么已经变成 native HTML(原生 HTML),要么已经变成 native CSS(原生 CSS),要么可以通过 HTML + CSS 组合完成,而且完全不需要 JavaScript。
这不是理论。
这些功能要么已经发布,要么正在进入 stable browser releases(稳定版浏览器发布)。
从现在开始,真正理解这些能力的开发者,会写出更轻、更快、更 accessible(可访问)的 UI(用户界面)。
下面就是你需要知道的部分。
CSS 终于有了真正的逻辑能力
if() function可能是开发者从 preprocessor era(预处理器时代)就一直想要的 CSS 特性。
你只需要在元素上设置一个 custom property(自定义属性),然后在 stylesheet(样式表)里直接用 if() 判断。
不需要 JavaScript toggle(脚本切换)。
不需要额外 class(类名)。
也不需要 React prop drilling(React 属性层层传递)。
.button {
background-color: if(style(--variant: primary): #6366f1; else: #e5e7eb);
color: if(style(--variant: primary): #ffffff; else: #111827);
padding: if(style(--size: large): 0.875rem 1.75rem; else: 0.5rem 1rem);
}
一个 component(组件)。
三个 style decisions(样式决策)。
HTML 只需要设置:
--variant: primary;
然后就结束了。
Native CSS @function(原生 CSS 函数)会把这件事推进得更远。
以前你经常从 Sass mixin(Sass 混入)或者 PostCSS plugin(PostCSS 插件)里复制的 fluid typography(流式字体)公式,现在终于可以直接待在 CSS 里。
@function --fluid(--min, --max, --from: 320px, --to: 1280px) {
result: clamp(
var(--min),
calc(var(--min) + (var(--max) - var(--min)) * ((100vw - var(--from)) / (var(--to) - var(--from)))),
var(--max)
);
}
h1 { font-size: --fluid(1.5rem, 3rem); }
h2 { font-size: --fluid(1.25rem, 2.25rem); }
p { font-size: --fluid(1rem, 1.125rem); }
不需要 build step(构建步骤)。
不需要 node_modules(依赖目录)。
就是 CSS 调用 CSS。
而即将落地的 native @mixin(原生混入)和 @apply(应用混入)会把这套能力补完整。
如果你写过 Sass,语法基本一眼就懂。
@mixin --glass {
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 1rem;
}
.card { @apply --glass; padding: 2rem; }
.drawer { @apply --glass; padding: 1.5rem 2rem; }
.toast { @apply --glass; padding: 0.75rem 1.25rem; }
三个 components(组件)。
一个 source of truth(单一真实来源)。
零 preprocessors(预处理器)。
Scroll State Queries:IntersectionObserver 的退休通知
这是近几年最容易被低估,但影响非常大的新增能力之一。
浏览器其实一直都知道这些状态:
sticky element(粘性元素)现在是不是真的 stuck(吸顶/固定住了)
scroll snap section(滚动吸附区块)是不是当前 snapped(吸附到位)
container(容器)是不是 overflowing(溢出)
只是过去浏览器没有把这些信息直接告诉你。
现在,它可以通过:
container-type: scroll-state
以及新的 @container condition(容器条件查询)暴露出来。
你可以像写 width breakpoint(宽度断点)一样查询滚动状态。
以前做一个 sticky nav(吸顶导航)滚动后缩小,通常需要:
IntersectionObserver(交叉观察器)
sentinel <div>(哨兵元素)
event listener(事件监听)
class toggle(类名切换)
现在只需要几行 CSS:
nav {
container-type: scroll-state;
position: sticky;
top: 0;
}
@container scroll-state(stuck: top) {
nav {
padding: 0.75rem 2rem;
background: rgba(15, 15, 20, 0.92);
backdrop-filter: blur(16px);
}
}
同样,它也能用于 scroll-snapped sections(滚动吸附区块)。
比如你想让标题只在当前 section(区块)成为 active snap target(当前吸附目标)时出现:
section {
container-type: scroll-state;
scroll-snap-align: start;
}
.headline {
opacity: 0;
transform: translateY(1.5rem);
transition: opacity 0.5s ease 0.2s, transform 0.5s ease 0.2s;
}
@container scroll-state(snapped: block) {
.headline {
opacity: 1;
transform: translateY(0);
}
}
Fullscreen scroll storytelling(全屏滚动叙事)。
Cinematic entrance animations(电影感入场动画)。
全部都可以不碰 JavaScript。
更符合直觉的形状控制
从 2011 年开始,border-radius(圆角)就一直是一个有点粗糙的工具。
它只能控制圆角半径,却不能真正控制角的 geometry(几何形状)。
新的 corner-shape property(角形状属性)解决了这个问题。
你可以控制的不再只是圆角大小。
而是角本身的形状。
squircle(超椭圆角)可以做出类似 Apple icon(苹果图标)的曲线。
notch(切角)可以斜切。
bevel(斜面角)可以压平。
scoop(内凹角)可以向内弯曲。
.badge {
corner-shape: squircle;
border-radius: 1.25rem;
background: #6366f1;
color: white;
}
.chip {
corner-shape: notch;
border-radius: 0.5rem;
}
.tag {
corner-shape: bevel;
border-radius: 0.375rem;
}
shape() function(形状函数)则走得更远。
它把类似 SVG path commands(SVG 路径命令)的可读语法,直接带进了 clip-path(裁剪路径)。
以前做一个 wavy section divider(波浪分割区域),你可能需要 inline SVG(内联 SVG),或者写一段没人愿意维护的 clip-path: polygon()。
现在可以这样:
.hero {
clip-path: shape(
from 0% 0%,
line to 100% 0%,
line to 100% 90%,
curve to 50% 100% with 75% 90% / 50% 100%,
curve to 0% 90% with 50% 100% / 25% 90%,
close
);
}
可读。
可编辑。
不需要从 Figma(设计工具)导出 SVG。
Popovers、Dialogs,以及 Overlay Libraries 的终点
Popover API(弹出层 API)已经获得了比较广泛的 browser support(浏览器支持)。
它正在真正淘汰一整类 JavaScript library(脚本库)。
你只需要给元素加上 popover attribute(弹出层属性),再让按钮的 popovertarget attribute(弹出目标属性)指向它的 ID,就能获得:
light-dismiss behavior(点击外部关闭)
focus management(焦点管理)
correct top-layer stacking(正确顶层堆叠)
这些过去都要靠 JS 库处理。
<button popovertarget="palette">Choose color</button>
<div id="palette" popover>
<div class="swatches">
<button style="--c: #ef4444"></button>
<button style="--c: #f97316"></button>
<button style="--c: #eab308"></button>
<button style="--c: #22c55e"></button>
<button style="--c: #6366f1"></button>
</div>
</div>
[popover] {
position-anchor: --trigger;
top: anchor(bottom);
left: anchor(left);
margin-top: 0.5rem;
border-radius: 0.75rem;
padding: 0.75rem;
background: #1e1e2e;
border: 1px solid rgba(255,255,255,0.1);
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
}
.swatches {
display: flex;
gap: 0.5rem;
}
.swatches button {
width: 2rem;
height: 2rem;
border-radius: 50%;
background: var(--c);
border: 2px solid transparent;
cursor: pointer;
transition: transform 0.15s, border-color 0.15s;
}
.swatches button:hover {
transform: scale(1.15);
border-color: white;
}
点击外部会自动关闭。
不需要:
document.addEventListener('click', ...)
也不需要:
z-index: 9999;
浏览器会处理这一切。
<dialog> element(对话框元素)也通过 Invoker Commands(调用器命令)获得了类似的 declarative upgrade(声明式升级)。
过去你写过无数次的 boilerplate(样板代码):
document.getElementById('modal').showModal()
现在可以变成两个 HTML attributes(HTML 属性):
<button command="show-modal" commandfor="confirm-dialog">Delete account</button>
<dialog id="confirm-dialog">
<p>This cannot be undone. Are you sure?</p>
<menu>
<button command="close" commandfor="confirm-dialog">Cancel</button>
<button value="confirm" autofocus>Yes, delete</button>
</menu>
</dialog>
focus trap(焦点陷阱)。
Escape key(Esc 键关闭)。
::backdrop(背景遮罩伪元素)。
全部都是 native(原生)。
全部免费。
不用再写 JavaScript plumbing(底层粘合代码)。
CSS Carousels:2000 行代码变成 200 行
Pinterest 已经实践过新的 CSS carousel primitives(CSS 轮播基础能力)。
他们把原本大约 2000 行的 JavaScript carousel(轮播逻辑),减少到了大约 200 行 CSS。
这些 primitives(基础能力)包括:
scroll markers(滚动标记)
scroll buttons(滚动按钮)
styleable scroll fragmentation concepts(可样式化的滚动分段概念)
这些能力浏览器内部其实早就知道,只是现在终于公开给开发者用了。
<div class="reel">
<button scroll-target="track" class="prev">←</button>
<ul id="track">
<li><img src="shot-1.jpg" alt="Mountain at dawn"></li>
<li><img src="shot-2.jpg" alt="Forest in fog"></li>
<li><img src="shot-3.jpg" alt="Desert at dusk"></li>
<li><img src="shot-4.jpg" alt="Ocean at night"></li>
</ul>
<button scroll-target="track" class="next">→</button>
</div>
#track {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
scrollbar-width: none;
list-style: none;
padding: 0;
margin: 0;
}
#track li {
flex: 0 0 100%;
scroll-snap-align: start;
}
#track img {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
border-radius: 1rem;
display: block;
}
::scroll-marker-group {
display: flex;
gap: 0.5rem;
justify-content: center;
margin-top: 1rem;
}
#track li::scroll-marker {
content: "";
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
background: rgba(255,255,255,0.35);
transition: background 0.2s, transform 0.2s;
}
#track li::scroll-marker:target-current {
background: white;
transform: scale(1.4);
}
浏览器会自动追踪当前 slide(幻灯片)。
并且自动更新:
::scroll-marker:target-current
不需要 activeIndex state(当前索引状态)。
不需要 resize observer(尺寸观察器)。
不需要手动管理小圆点。
不碰 DOM 的高亮能力
Search highlighting(搜索高亮)看起来像个小问题。
直到你真的实现它。
如果你用 <mark> tag(标记标签)包住匹配文本,就会遇到一堆麻烦:
拆分 text nodes(文本节点)
mutating DOM(修改 DOM)
处理跨多个元素的匹配
比如一句话中间有个 <strong> tag(加粗标签),匹配刚好跨过去。
Custom Highlight API(自定义高亮 API)直接换了一种思路。
你在 JavaScript 里定义 Range objects(范围对象),把它们注册到一个 named highlight(命名高亮)里,然后用 CSS 的 ::highlight() pseudo-element(高亮伪元素)来设置样式。
DOM(文档对象模型)完全不用动。
function highlight(query, root) {
CSS.highlights.clear();
if (!query.trim()) return;
const ranges = [];
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
let node;
while ((node = walker.nextNode())) {
const text = node.textContent.toLowerCase();
const q = query.toLowerCase();
let i = 0;
while ((i = text.indexOf(q, i)) !== -1) {
const range = new Range();
range.setStart(node, i);
range.setEnd(node, i + q.length);
ranges.push(range);
i++;
}
}
CSS.highlights.set("match", new Highlight(...ranges));
}
::highlight(match) {
background-color: #fef08a;
color: #1e1e2e;
}
它可以处理跨元素边界。
它只是 paint-only(仅绘制层处理),不会触发 layout recalculation(布局重新计算)。
当你有成千上万个匹配项时,它也比 DOM mutation approach(DOM 修改方案)快得多。
不用 SPA 也能做页面转场
多年来,single-page apps(单页应用)相比 traditional websites(传统多页网站)有一个明显视觉优势:
animated page transitions(页面转场动画)。
内容可以在 routes(路由)之间滑动、淡入淡出、形变,而不是硬切页面。
现在,View Transitions(视图转场)正在把这个差距抹平。
而且它适用于 multi-page apps(多页应用)。
你只需要在每个页面加两行 CSS,就能获得 navigation(页面导航)之间的 cross-fade(交叉淡入淡出):
@view-transition {
navigation: auto;
}
你也可以通过 targeting transition pseudo-elements(定位转场伪元素)来自定义动画:
::view-transition-old(root) {
animation: fade-slide-out 0.25s ease-in forwards;
}
::view-transition-new(root) {
animation: fade-slide-in 0.25s ease-out forwards;
}
@keyframes fade-slide-out {
to {
opacity: 0;
transform: translateY(-1rem);
}
}
@keyframes fade-slide-in {
from {
opacity: 0;
transform: translateY(1rem);
}
}
如果你在 source page(来源页面)和 destination page(目标页面)上给同一个元素设置相同的 view-transition-name(视图转场名称),浏览器会单独为这个元素做动画。
它会从旧位置移动到新位置。
跨页面也能平滑过渡。
/* On the product grid page */
.product-card[data-id="42"] img {
view-transition-name: product-hero;
}
/* On the product detail page */
.hero-image {
view-transition-name: product-hero;
}
图片会从 product card(商品卡片)飞到 detail page(详情页)。
不需要 JavaScript。
不需要 SPA(单页应用)。
不需要 shared element animation library(共享元素动画库)。
浏览器会计算起点和终点,然后自动插值动画。
你只需要让它做它该做的事。
最后
这些新能力并不意味着你以后写的代码总量一定会变少。
真正的变化是:
你会写更正确的代码。
你会把精力放在真正解决问题的地方。
而不是继续手写那些本来就该由浏览器处理的 scaffolding(脚手架代码)。
该文章在 2026/8/11 10:47:48 编辑过