你有没有发现这两年写前端的人越来越不爱为一个小按钮特效挂一堆 JavaScript 监听器了。CSS 能管的事正在肉眼可见地变多。2025 年的 Chrome已经能把很多“交互”直接写进样式表里围绕这些新能力形成的开发方式圈子里有个名字叫“声明式 Web”。这篇文章想聊聊我最近的亲身体会为什么说 2025 年是声明式 Web 真正发力的年份以及这些新 CSS 特性到底怎么落到真实项目里而不是停留在收藏夹里吃灰。声明式 Web 不是某个单一功能而是一整套开发范式的转变。过去我们习惯用 JS 一步步操作 DOM告诉浏览器“先做这个、再做那个”现在 CSS 吸收了越来越多原本属于 JS 的能力让我们直接描述“最终是什么样”浏览器自己搞定中间过程。这对生产项目的影响是实打实的代码量少了、交互更顺了、维护成本也降了。不管你是写业务页面的前端还是做可视化大屏、小游戏、组件库的开发者这篇文章里的内容都能直接抄走用。1. 声明式 Web把“怎么做”交给浏览器1.1 声明式与命令式差的就是那口锅我经常用一个点外卖的类比来说这个事。命令式开发就像自己下厨开火热油、洗菜切菜、下锅翻炒、调火候、装盘每一步都要你亲自盯着稍微走神菜就糊了。声明式开发像点外卖你告诉老板“来一碗牛肉面”剩下的备料、火候、出锅全由后厨完成。前端过去十年一直在从“下厨模式”往“点单模式”搬。React 把视图变成了声明的 UI 描述Vue 用模板语法做了类似的事但在我们天天写的 CSS 上这个过程走得比想象中慢。CSS 从诞生起就是声明式的但它能声明的范围太窄。过去 CSS 的状态只有 hover、active、focus 这几个能动的属性也主要是 transform、opacity、颜色这类基础样式。想做个涟漪扩散、流光边框、数字滚动都得靠 JS 创建元素、计算位置、管理生命周期。2025 年再回头看事情已经不一样了CSS 自定义属性有了类型系统可以参与插值动画滚动进度可以直接驱动动画时间线页面切换也能用纯声明方式描述过渡效果。浏览器把很多“怎么做”包揽了开发者只需要声明“想要什么”。1.2 为什么是 2025 年而不是前几年关键推进来自几个方面。首先是 Chrome 的迭代节奏每四周一个稳定版2025 年初已经到了 Chrome 133 左右按这个速度年底会走到 14x。新特性从实验到默认开启的周期大幅缩短。其次是 Chromium 主导的 Interop 项目它联合各浏览器厂商对齐 Web 平台行为让新特性不再是“Chrome 专属”而是整个生态的公共基础设施。再加上 WebDX 社区推广的 Baseline 概念标注了某个特性从什么时候开始被所有主流浏览器默认支持开发者不用再对着 caniuse 猜半天。2025 年这批特性有几个特别值得关注CSS 锚点定位正式落地滚动驱动动画从试验走向稳定跨文档视图过渡可用property 经过几年普及已经成为新特效的标配。把它们连起来看CSS 已经从“样式语言”悄悄变成了“应用逻辑语言”。我们正处在一个转折点上以前写交互必须先想 JS现在可以先把 CSS 能做的部分榨干不够了再用 JS 补充。这就是声明式 Web“革命之年”的真正含义。2. 2025 年顺手能用的几个新 CSS 能力2.1 propertyCSS 变量终于有类型了property 是这两年影响最深的一个特性它让自定义属性从“纯字符串”变成了“有类型的变量”。过去 CSS 变量玩不出复杂动画因为浏览器不知道它是一个长度、一个角度还是一个数字没法做插值。现在可以用 property 注册一个变量明确告诉浏览器它的语法类型、初始值、是否继承property --ripple-x { syntax: length-percentage; inherits: false; initial-value: 50%; } property --angle { syntax: angle; inherits: false; initial-value: 0deg; }注册之后这个变量就能像 transform 里的数值一样被动画驱动。这个能力和 CSS 动画、过渡配合起来等于给设计系统开了一扇门你可以把整个视觉状态抽象成几个自定义属性然后让它们自己动起来。我自己的项目里导航栏高亮、按钮按下反馈、卡片悬停光效都是靠 property 实现的。没有这个能力后面要聊的涟漪、流光、数字动画全都是空中楼阁。2.2 三角函数、相对颜色与混合函数还有一个容易被忽略的变化是 CSS 里终于能算数学了。sin()、cos()、tan() 可以直接用在 calc() 里这意味着很多以前必须交给 JS 的几何计算现在纯 CSS 就能完成。比如做一个圆形的点阵分布以前要遍历数组算坐标现在可以这样写.dot { position: absolute; left: calc(50% 120px * var(--radius-factor) * cos(var(--angle))); top: calc(50% 120px * var(--radius-factor) * sin(var(--angle))); }配合 property 注册 --angle还能做出点阵旋转的动画。另外color-mix() 函数可以按比例混合颜色light-dark() 可以依据系统主题自动切换深浅色这些能力把原先需要 JS 加一堆工具函数、CSS 变量覆盖的活都简化了。我做主题切换的时候现在只需要在 CSS 里写一句color: light-dark(#222, #f5f5f5)浏览器自动适配。2.3 :has() 选择器和子网格选择器方面:has() 是个大招。它让 CSS 具备了“根据子元素状态反推父元素样式”的能力过去要实现这种逻辑得在 JS 里观察 DOM 变化或者在 HTML 里拼条件类名。现在可以直接声明.card:has(.badge) { border-color: var(--accent); } .form-field:has(input:invalid) { box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.3); }父元素能感知子元素状态这是一次思维转变CSS 不再只是从外到内地渲染也能从内到外地响应。配套的 subgrid 则解决了嵌套网格的痛点。以前子网格要自己重新定义轨道和父网格经常对不齐现在grid-template-columns: inherit或者用subgrid让子网格直接复用父网格的轨道划分。做复杂表单、仪表盘这类对齐要求高的界面省太多事了。3. 经典特效的声明式改造现场3.1 涟漪光圈扩散不再需要创建销毁节点先聊一个大家在热搜词里经常搜的效果点击按钮时那个光圈扩散的涟漪。如果搜过老教程你会看到一套标准答案监听 click 事件创建一个 span定位到鼠标坐标添加动画类监听 animationend 再移除节点。这个做法能用但代码量不少而且频繁创建销毁 DOM 节点会带来额外开销。2025 年的做法是JS 只负责传递坐标剩下的动画全交给 CSS。.ripple-btn { position: relative; overflow: hidden; } .ripple-btn::after { content: ; position: absolute; left: var(--x, 50%); top: var(--y, 50%); width: 20px; height: 20px; border-radius: 50%; background: radial-gradient(circle, rgba(255, 255, 255, 0.5) 0%, transparent 70%); transform: translate(-50%, -50%) scale(0); opacity: 1; transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.6s ease; pointer-events: none; } .ripple-btn:active::after { transform: translate(-50%, -50%) scale(25); opacity: 0; }这段代码只处理了“按下时从中心扩散”的情况好处是零 JS。如果你希望涟漪从鼠标按下的精确位置开始扩散JS 也不是完全没有但也只剩三行btn.addEventListener(click, (e) { const r btn.getBoundingClientRect(); btn.style.setProperty(--x, ${e.clientX - r.left}px); btn.style.setProperty(--y, ${e.clientY - r.top}px); });扩散、缩放、淡出这些过程全部由 CSS transition 接管。这里有个实用细节我给伪元素加了pointer-events: none防止它挡住按钮自身的点击区域过渡曲线选了cubic-bezier(0.22, 1, 0.36, 1)前段加速、后段平缓扩散的“扑开感”最自然。3.2 流光边框渐变与角度变量的组合拳“流光边框”是评论区经常被问的效果。实现的原理并不玄妙给元素准备两层背景一层作为填充底色一层作为边框渐变然后用 property 注册的角度变量驱动整个渐变旋转。代码是这样property --angle { syntax: angle; inherits: false; initial-value: 0deg; } .card { border: 2px solid transparent; background: linear-gradient(var(--surface-color), var(--surface-color)) padding-box, conic-gradient( from var(--angle), #ff6b6b, #f59e0b, #10b981, #3b82f6, #ff6b6b ) border-box; animation: rotate-border 4s linear infinite; } keyframes rotate-border { to { --angle: 360deg; } }这张卡的背景有两层第一层是普通渐变色贴在 padding-box 上负责把内容区域填实第二层 conic-gradient 贴在 border-box 上让渐变色从边框区域透出来。因为注册了 --angle 属性animation可以把角度从 0 一直转到 360 度视觉上就是一圈光在边框上循环流动。这里有一个经常翻车的点如果不设置 padding-box 和 border-box 的区分渐变会把整个卡片盖住内容看不清。还有就是把property --angle的inherits设为 false避免子元素的动画互相干扰。实际测试下来这张卡最好用深色背景浅色背景上流光效果对比度不够容易显得脏。3.3 渐变字体和纯 CSS 数字倒计时渐变字体的经典写法是 background-clip: text这个很多人已经会了。但把它和 property 结合可以做更多比如文字颜色随滚动渐变、随主题切换、随数据变化。基础代码.gradient-text { background: linear-gradient(120deg, #f66, #f90, #3b82f6); -webkit-background-clip: text; background-clip: text; color: transparent; }有几个坑要提醒一下背景图尺寸至少 200% 然后位移渐变才会有流动的空间color: transparent 之后如果有文字描边等辅助装饰注意优先级另外必须保留 fallback极少数环境不支持 background-clip: text 时文字不能直接消失建议前面加一层supports (background-clip: text)判断。数字倒计时也是一个典型案例。热搜里“数字加载动画效果 css”问的人很多其实可以用 property 加 CSS 计数器来完成property --num { syntax: integer; inherits: false; initial-value: 0; } .counter::before { counter-reset: number var(--num); content: counter(number); animation: count-up 3s forwards; } keyframes count-up { to { --num: 100; } }是的这个倒计时里完全没有 JS。浏览器会在动画期间对 --num 做整数插值counter() 再把数值渲染成文本。这套写法在排行榜、数据大屏、投票结果里通用而且计数过程在合成器线程完成卡顿概率远低于 JS 操作 DOM 文本。4. 从点状特效到整页体验滚动驱动与视图过渡4.1 滚动进度条不用监听 scroll 事件以前做一个“阅读进度条”要监听滚容器的 scroll 事件计算文档高度和视口高度的比例再写到元素的 style 上。性能差点还得加节流一不留神就会产生滚动卡顿。现在 CSS 的滚动驱动动画直接把这个过程声明成了样式.progress-bar { position: fixed; top: 0; left: 0; height: 4px; width: 100%; background: linear-gradient(90deg, var(--primary), var(--accent)); transform-origin: 0 50%; animation: progress-grow linear both; animation-timeline: scroll(root); } keyframes progress-grow { from { transform: scaleX(0); } to { transform: scaleX(1); } }animation-timeline: scroll(root)告诉浏览器把页面根容器从顶部滚到底部的整个过程映射到动画进度上。both保证动画在滚动前和滚动后都保持首尾状态。这样滚动进度条不仅代码少了计算也移到了浏览器的合成线程帧率更稳。这个能力还可以套用到很多场景产品详情页里根据图片滚动位置改变背景色、导航栏距离顶部多少时变成毛玻璃、侧边栏高亮跟随滚动切换。注意一点滚动驱动动画和普通的 animation 一样受prefers-reduced-motion影响做无障碍适配的时候记得留一手。4.2 View Transitions页面切换也能声明式描述2025 年值得重点练的是 View Transitions。以前做页面切换的过渡动画要么引入一个动效库要么自己写 JS 记录旧节点、拍快照、做动画、再清理。现在浏览器直接提供了文档级视图过渡能力核心用法非常简单调用document.startViewTransition()然后把 DOM 更新放在回调里整个过渡动画默认就能工作。Web 上可以嵌套更复杂的场景比如从列表页到详情页图片从一个位置“飞到”另一个位置。当然它也能用 CSS 自定义每个阶段的动画。SSR 或 MPA 项目里Chrome 支持跨文档视图过渡直接在 CSS 里写::view-transition-old(root) { animation: fade-out 0.3s ease; } ::view-transition-new(root) { animation: fade-in 0.3s ease; }这套机制对用户的感知提升非常明显尤其在上一个页面内容较多时平滑退出和进入会让应用显得精致。我自己的站点在接入之后肉眼可见地减少了“硬跳”的粗糙感。不过要注意别把过渡时间拉太长300ms 左右是最舒服的区间超过 600ms 用户就会觉得拖沓。4.3 鼠标移入不是“事件”而是“状态”热搜里“css 鼠标移入事件”是个常见搜索词。这其实混淆了一个概念鼠标移入在 CSS 里不是事件而是状态。事件是一次性的触发状态则持续存在直到条件消失。CSS 的:hover、:focus-within、:active描述的都是状态。理解这一点之后很多 JS 里需要手动管理的事件在 CSS 里都可以简化成状态层的自动响应。比如一张卡片鼠标移入时图片放大、阴影加深、按钮出现只要写状态对应的规则.card:hover img { transform: scale(1.05); } .card:hover .card-actions { opacity: 1; transform: translateY(0); }不需要 JS 去 addEventListener也不需要担心忘记移除监听器。类似“CSS 能实现屏幕穿出来的效果吗”完全可以靠 transform 和 clip-path 做让元素从视口边缘的裁剪状态过渡到完全展示视觉上就是“穿出屏幕”。这些效果在苹果官网、设计工作室主页上很常见2025 年的 CSS 做起来已经非常轻松。5. 布局与游戏级前端从 1440x810 适配说起5.1 布局方式选型与对比CSS 的布局体系现在已经相当完整普通流、浮动、定位、Flex、Grid 五种方式前两种属于基础认知在实际项目里应该把重心放在 Flex 和 Grid 上。浮动float只有在处理图文混排时还有价值普通流是页面默认的上下文绝对定位适合弹层、角标这类脱离文档流的内容。做整页布局我基本只用 Grid做组件内部排列优先 Flex需要自由定位就配合 absolute。五种方式说到底不是选择题而是组合题。一个现代页面的骨架通常长这样Grid 定义整体行与列Flex 处理导航栏内部的项目排列absolute 定位浮层按钮普通流负责正文段落。2025 年还有一个新选择Subgrid它让 Grid 可以嵌套出现在父网格的轨道里做复杂报表和移动端分栏时非常好用建议找个小项目先试试水。5.2 1440x810 整屏容器适配“植物大战僵尸 html 完整代码”这类热搜词看起来是在做一个全屏游戏但背后的技术问题是通用如何把一套固定宽高的页面比如 1440x810完整适配到不同屏幕上。如果只是居中显示用 scale 方式最直接.game { width: 1440px; height: 810px; transform: scale(min(100vw / 1440, 100vh / 810)); transform-origin: top left; }这段代码的意思是取视口宽度除以 1440 和视口高度除以 810 两个比值之中的较小值整体等比缩放原点定在左上角。为了让它居中需要用一个小技巧外层包裹容器用 flex 居中然后给 .game 加一个反向 margin 补偿缩放后占据的额外空间。相比改 rem、做响应式断点这种 scale 方案适合游戏场景、会议大屏展示、在线演示文稿。缺点也很明显字体和图片会稍微模糊因此只适合固定尺寸的游戏或演示不适合内容型页面。如果你要做的是真正响应式应该用容器查询而不是视口查询。容器查询让子组件根据最近的容器尺寸调整布局配合 container 指令组件的复用性会大幅提升。我最近把组件库里的面板重写了一遍小容器里自动堆叠、大容器里变侧边栏调用方完全不用设置断点类名。5.3 原子性 CSS 与组件化其实不冲突热搜里还有“原子性 css”这个词。现阶段最火的原子化方案是 Tailwind CSS 这类 utility-first 工具。它的思路是把样式声明写成 HTML 里的类名比如 flex、items-center、px-4、bg-red-500看起来像内联样式但背后有设计系统约束。这和“声明式 Web”的关系很有意思以前在样式文件里声明现在在结构里声明本质还是在描述“长什么样”而非“怎么画出这个样”。我见过不少团队纠结要不要上 Tailwind。我的建议是如果你们项目里有复杂的设计系统或大量可视化样式原子化 CSS 配合组件库反而是省力的。它把样式收敛到 HTML避免了一人一个写法的混乱。但也不要迷信所有东西都原子化涉及第三方 UI 库的覆盖、复杂伪类状态和动画标准 CSS 依然重要。6. 开发日常避坑Chrome 里的常见问题速查6.1 CSS 文件到底要不要写 style 标签这个热搜词看起来基础到不能再基础但遇到的人真不少。结论很明确独立的外部 CSS 文件里不需要写style直接写 CSS 规则即可。style是 HTML 里的标签只出现在 HTML 文件内部。外部文件通过link relstylesheet href...引入样式文件本身的内容应该是body { ... }这样的规则而不是包一层style。如果你在 .css 文件里写了style浏览器会把它当普通文本或选择器处理什么都匹配不上。至于内联样式、阴影 DOM 里的样式、通过 JS 动态插入的样式各有各的写法。如果项目用了 Web ComponentsShadow DOM 内部还是需要style或 constructable stylesheet。核心原则是样式文件优先用 link 引入利于缓存和并行加载个项目里的关键 CSS 可以做内联优化但不要因此放弃外部文件的管理结构。6.2 Chrome 高频问题排查速查表这段针对的是 Chrome 使用中真正高频的问题基本每个团队都遇到过。现象常见原因处理建议启用硬件加速后光标变白GPU 驱动兼容性问题先在设置里关闭硬件加速验证再尝试更新显卡驱动提示 Chrome 版本太旧停更或系统限制新版本安装优先考虑升级系统企业环境可用策略静默更新不建议长期用旧版本访问网银等业务Chrome 无法上网代理设置残留、缓存损坏、系统代理冲突检查系统代理刷新 DNS 缓存必要时清理浏览器缓存并重设 network settings滚动慢软件渲染、扩展干扰、平滑滚动 flag关闭可疑扩展检查chrome://flags里平滑滚动相关项尝试开启或关闭硬件加速对比视频卡顿缺解码器、GPU 加速未启用、网络不稳开启硬件加速检查 GPU 驱动禁用潜在的广告拦截扩展做排查扩展提示“不受支持的清单版本”清单 v2 扩展被 Chrome 移出支持范围改用 Manifest V3 扩展寻找同功能的 MV3 替代方案这里提醒一句chrome://net-internals/#hsts是查看和删除 HSTS 记录的调试页面。普通用户不遇到“域名行为诡异跳转 HTTPS”的情况基本用不到也别听教程乱操作删错记录反而可能造成访问异常。开发调试时记住它存在即可。6.3 新特性进入生产前先做降级检查CSS 新特性再香也要考虑没有支持的浏览器怎么体验。我现在的习惯是每个新特性都要问三个问题第一如果这个动画失效页面核心功能是否受影响第二是否有 supports 或者特性检测兜底第三动画是否尊重用户的减弱动效设置。特效归特效可用性归可用性两者要分开。比如滚动驱动动画虽然现代浏览器都支持了但 Safari 的某些旧版本对animation-timeline可能还停留在实验状态所以我的进度条组件里会先写一个默认无动画版本再用supports (animation-timeline: scroll())包一层高级效果。这样特性不可用时用户看到的只是少了个进度条但正文阅读完全没有障碍。7. 周末动手把一个小特效改造成声明式写法如果你看完前面这些内容想找一个最小成本的项目练手我强烈建议从“按钮点击涟漪”开始。理由有三它足够独立不会牵一发而动全身它的旧实现大多是 JS对比感受最明显它花不了二十分钟却能让你快速理解 property 和状态驱动这两个核心思想。改造步骤很简单。先把原来的 JS 代码拆开找出哪些是在创建节点、哪些是在控制动画、哪些是在清理。创建和清理的部分换成 CSS 伪元素动画控制的部分换成 transition 里设置起点和终点只有鼠标坐标需要动态信息保留 JS 一行更新 --x 和 --y。跑起来之后你就感受到了CSS 代码比原来短一半而且放大缩小过程中几乎不会有主线程占用。我还有一个习惯改造完用 Chrome 的 Performance 面板录一段操作观察有没有长任务。好的声明式实现应当干干净净长任务不超过几十毫秒。对比之下以前那些绑在 scroll 和 mousemove 上的逻辑可能已经悄悄是几百毫秒的包袱。对我个人来说这些年写前端最大的变化就是心态上的变化遇到交互效果我没有第一时间去查某个库的 API而是先问自己能不能用 CSS 把状态变化描述出来。这背后是 CSS 能力边界的扩展也是整个 Web 平台在往更“声明式”的方向演进。2025 年Chrome 把一整套新特性推到了稳定状态剩下的就是我们在真实项目里慢慢把手里的 JS 卸载掉。建议你也找一个周末挑个用了很久的按钮特效用今天这些写法重写一遍你会上瘾的。
阅读完成 · 觉得有帮助?