写在前面
动画系周,最后一天。
这一周我们分别攻下了六种动画:
| 效果 | 核心技法 |
|---|---|
| 打字机 | ch + steps() + delay排时间轴 |
| 六种加载器 | border / conic / scaleY / background-position |
| 烟花 | box-shadow粒子群 + 核心scale |
| 拟物怀表 | conic-gradient拉丝金属 + 体外支点 |
| 流光签名 | background-clip: text + stroke-dashoffset |
| 滚动叙事 | animation-timeline: scroll(root) / view() |
今天把它们缝成一个完整的落地页Hero区。
一篇"分镜表"驱动的产品首屏动画。
这一篇交付的不是"一个新效果",而是一种编排方法:
- 分镜表怎么画------delay从0累加,动笔之前先定表
- 双动画怎么交棒 ------
to必须等于下一棒的from - 节奏密度怎么定------相邻分镜间隔0.15 ~ 0.35s
Hero的质感,20%来自编排,80%来自光照纪律。
一、先看效果

深色发布会现场:远处一团极光缓慢呼吸,脚下网格地板向灭点收缩。
页面打开的瞬间像有人按了播放键------2秒内依次发生:
- 0.10s :绿字kicker(
AURORA AUDIO · NEW)先亮起 - 0.35s:大标题「SOUND, FROZEN IN LIGHT.」从容升起
- 0.70s:中央的翡翠球体带着两圈反向旋转的轨道环放大入场
- 1.60s:球体进入"永续悬浮"------轻轻上下呼吸
- 0.95s ~ 1.25s :三枚参数
24H / 38G / LDAC依次错峰浮起 - 1.40s :最后一枚「立即预约」按钮带一点过冲弹出来
全程2秒,之后页面只留下低速呼吸------球体悬浮、轨道环自转、极光漂移。
刷新页面可重看入场。
怎么跑起来:
- 新建一个
demo.html - 把下面第二节的完整代码粘进去
- 双击用浏览器打开
📸 截图党提示 :录3秒 GIF 抓从页面打开到CTA弹出的完整入场最生动。静态图选两张拼图------入场中段 (球体正在放大、参数浮了一半)+ 入场完毕(全部到位)。
二、完整代码(复制即用)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Day 21 · 周挑战:落地页 Hero 动画</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
html { overflow-x:hidden; }
body { min-height:100vh; overflow-x:hidden; background:#070a12;
font-family:"PingFang SC", sans-serif; color:#e6edf3; }
/* ------ 氛围:极光带 + 网格地板 ------ */
.aur { position:fixed; inset:0; overflow:hidden; }
.aur i { position:absolute; right:-6%; top:-14%; width:560px; height:300px; border-radius:50%;
background:radial-gradient(ellipse, rgba(13,133,116,.4), transparent 65%);
filter:blur(70px); animation: drift 16s ease-in-out infinite alternate; }
@keyframes drift { to { transform:translate(-70px,40px) scale(1.15); } }
.floor { position:fixed; left:-20vw; right:-20vw; bottom:-6vh; height:36vh;
background:
linear-gradient(rgba(125,211,252,.07) 1.5px, transparent 1.5px),
linear-gradient(90deg, rgba(125,211,252,.07) 1.5px, transparent 1.5px);
background-size:54px 34px;
transform:perspective(600px) rotateX(62deg); transform-origin:top center;
-webkit-mask:linear-gradient(#000 30%, transparent 92%);
mask:linear-gradient(#000 30%, transparent 92%);
pointer-events:none; }
main { position:relative; z-index:2; min-height:100vh;
display:flex; flex-direction:column; align-items:center; justify-content:center;
padding:48px 20px; }
/* ------ 分镜 1 · 0.10s:kicker 亮起 ------ */
.kicker { font:400 11px/1 "SF Mono", Consolas, monospace; letter-spacing:.34em;
color:#5cc9b4; opacity:0; transform:translateY(12px);
animation:in .5s ease .1s both; }
/* ------ 分镜 2 · 0.35s:主标题升起 ------ */
h1 { margin-top:16px; text-align:center;
font-size:clamp(38px, 6vw, 56px); font-weight:600; line-height:1.14; letter-spacing:.02em;
opacity:0; transform:translateY(28px);
animation:in .7s cubic-bezier(.2,.8,.2,1) .35s both; }
/* ------ 分镜 3 · 0.70s:产品球体入场,1.60s 起持续悬浮 ------ */
.stage { position:relative; width:320px; height:240px; margin-top:10px;
perspective:900px; transform-style:preserve-3d; display:grid; place-items:center; }
.orb { width:180px; height:180px; border-radius:50%;
background:radial-gradient(circle at 33% 28%, #d8f7ec, #0d8574 48%, #073f37 82%);
box-shadow:0 0 60px 10px rgba(13,133,116,.35), inset -14px -18px 40px rgba(0,0,0,.45);
opacity:0; transform:scale(.55) translateY(36px);
animation:in .8s cubic-bezier(.2,.8,.2,1) .7s both, bob 5s ease-in-out 1.6s infinite alternate; }
@keyframes bob { from { transform:translateY(0); } to { transform:translateY(-14px); } }
/* 轨道环:常驻自转(0s 起转,0.95s 才淡入) */
.ring { position:absolute; left:50%; top:50%; border-radius:50%;
border:1.5px solid rgba(125,211,252,.45); opacity:0;
animation:fade .7s ease .95s both; }
.r1 { width:310px; height:100px; margin:-50px 0 0 -155px;
animation:fade .7s ease .95s both, spinz1 12s linear infinite; }
.r2 { width:250px; height:82px; margin:-41px 0 0 -125px;
border-color:rgba(255,194,207,.35);
animation:fade .7s ease 1.1s both, spinz2 9s linear infinite; }
@keyframes fade { from { opacity:0; } to { opacity:1; } }
@keyframes spinz1 { from { transform:rotateX(74deg) rotateZ(0deg); }
to { transform:rotateX(74deg) rotateZ(360deg); } }
@keyframes spinz2 { from { transform:rotateX(70deg) rotateZ(0deg); }
to { transform:rotateX(70deg) rotateZ(-360deg); } }
/* ------ 分镜 4 · 0.95s 起:三枚参数错峰浮起 ------ */
.specs { display:flex; gap:36px; margin-top:14px; }
.s { text-align:center; opacity:0; transform:translateY(14px); animation:in .5s ease both; }
.s:nth-child(1) { animation-delay:.95s; }
.s:nth-child(2) { animation-delay:1.1s; }
.s:nth-child(3) { animation-delay:1.25s; }
.s b { display:block; font:600 24px/1.2 "SF Mono", Consolas, monospace; color:#e6edf3; }
.s span { font-size:11px; letter-spacing:.22em; color:#5b6878; }
/* ------ 分镜 5 · 1.40s:CTA 过冲弹出 ------ */
.cta { margin-top:30px; padding:14px 40px; border-radius:99px; cursor:pointer;
border:1px solid rgba(92,201,180,.5); background:rgba(13,133,116,.18);
color:#bfeedf; font-size:14px; letter-spacing:.2em;
opacity:0; transform:scale(.9);
animation:pop .6s cubic-bezier(.34,1.56,.64,1) 1.4s both;
transition:background .25s, box-shadow .25s; }
.cta:hover { background:rgba(13,133,116,.34); box-shadow:0 0 30px rgba(13,133,116,.35); }
@keyframes pop { to { opacity:1; transform:scale(1); } }
/* ------ 统一入场帧:初始态写在元素上,这里只管「到位」------ */
@keyframes in { to { opacity:1; transform:none; } }
.tag { position:fixed; right:18px; bottom:14px; font:400 10px/1 "SF Mono", Consolas, monospace;
letter-spacing:.28em; color:rgba(230,237,243,.2); }
</style>
</head>
<body>
<div class="aur"><i></i></div>
<div class="floor"></div>
<main>
<div class="kicker">AURORA AUDIO · NEW</div>
<h1>SOUND, FROZEN<br>IN LIGHT.</h1>
<div class="stage">
<i class="ring r1"></i>
<i class="ring r2"></i>
<div class="orb"></div>
</div>
<div class="specs">
<div class="s"><b>24H</b><span>续航</span></div>
<div class="s"><b>38G</b><span>重量</span></div>
<div class="s"><b>LDAC</b><span>编码</span></div>
</div>
<button class="cta">立即预约</button>
</main>
<div class="tag">每天一个惊艳 CSS · DAY 21 · WEEK 3 FINALE</div>
</body>
</html>
三、三个核心技法拆解
今天和前几篇不一样------它不讲新效果,讲的是"编排方法"。
三个技法,从"心法"到"技巧"到"审美",层层递进。
技法1:先画分镜表,再写第一行CSS
这是本篇最重要的工程心法。
很多同学做入场动画的方式 ------想到哪写到哪:给kicker加个delay、给标题加个delay、给按钮加个delay......最后发现时间点乱成一锅粥。
正确做法 :先在注释里立一张分镜表。
分镜表 ------ 入场编排
| # | 元素 | 入点 | 时长 | 缓动 | 之后 |
|---|---|---|---|---|---|
| 1 | kicker | 0.10s | 0.5s | ease | - |
| 2 | 主标题 | 0.35s | 0.7s | 收尾减速 | - |
| 3 | 球体 | 0.70s | 0.8s | 收尾减速 | 悬浮 ∞ |
| 3+ | 轨道环 | 0.95s | 0.7s | ease | 自转 ∞ |
| 4 | 参数 ×3 | 0.95s | 0.5s | ease | - |
| 5 | CTA | 1.40s | 0.6s | 过冲曲线 | - |
然后逐行翻译成animation-delay------代码一行不多、一行不少。
这张表就是整篇文章的"设计稿"。
表格里最值钱的一列,是"入点"------相邻分镜的间隔:
| 相邻间隔 | 观感 |
|---|---|
| < 0.1s | 像事故------元素"挤"在一起出现 |
| 0.15 ~ 0.35s | 甜点位------前一个刚站稳、后一个接上 |
| > 0.5s | 像卡顿------用户以为页面加载失败了 |
本页的间隔:
- kicker → 标题 :
0.35 - 0.10 = 0.25s(甜点位) - 标题 → 球体 :
0.70 - 0.35 = 0.35s(稍长------因为标题是核心,多给点呼吸) - 球体 → 参数 :
0.95 - 0.70 = 0.25s(球体入场和参数入场重叠------这是刻意的) - 参数 → CTA :
1.40 - 1.25 = 0.15s(CTA是最后出场,节奏加快)
注意"重叠"这件事 ------球体还在放大(0.70s ~ 1.50s),参数已经在浮起(0.95s起)。
完全不重叠 ------观感像"报菜名",一样一样来。
适度重叠------观感像"编舞",有节奏感。
**"重叠"是分镜表里最优雅的部分------**它让整个入场不是"串行",而是"有节奏的并行"。
技法2:双动画交棒------to必须等于下一棒的from
球体挂了两条动画:
css
.orb {
opacity: 0;
transform: scale(.55) translateY(36px); /* 初始态 */
animation:
in .8s cubic-bezier(.2,.8,.2,1) .7s both, /* 入场:0.7s起,0.8s长 */
bob 5s ease-in-out 1.6s infinite alternate; /* 悬浮:1.6s起,无限 */
}
@keyframes in { to { opacity: 1; transform: none; } } /* 终点:正常态 */
@keyframes bob { from { transform: translateY(0); } /* 起点:正常态 */
to { transform: translateY(-14px); } }
两条动画的关系:
in:0.7s ~ 1.5s播放一次,把球体从"缩小 + 下移"拉到"正常"bob:1.6s起,无限循环,在"正常"和"上移14px"之间往复
为什么能无缝衔接?
因为两条动画的"交棒点"严格对齐:
| 时刻 | in的状态 |
bob的状态 |
球体实际状态 |
|---|---|---|---|
1.5s |
播放结束:translateY(0) |
尚未开始 | 正常态 |
1.6s |
已结束(forwards保持终态) |
开始:translateY(0) |
正常态 |
1.6s+ |
--- | 从0 → -14px |
悬浮 |
bob的from: translateY(0)精确等于in的to: transform: none ------这就是"交棒"。
交棒不齐会怎样?
css
/* ❌ 交棒不齐:in结束时还在translateY(20px),bob从0起跳 */
@keyframes in { to { transform: translateY(20px); } }
@keyframes bob { from { transform: translateY(0); } }
结果 :在1.5s → 1.6s那个瞬间,球体会**"跳"一下**------从+20px瞬间跳到0。
这一跳非常明显------观众说不清哪里不对,但会感到"不专业"。
通用心法:
两条动画交棒时,前一棒的
to必须精确等于后一棒的from。
数值要一个像素一个像素地对齐 ------包括translateY、scale、opacity、rotate 全部。
另一种做法:让两条动画"分工"。
css
/* ✅ 一条管opacity,一条管transform------永不冲突 */
.orb {
animation:
fade-in .8s ease .7s both, /* 只改opacity */
bob 5s ease-in-out 1.6s infinite alternate; /* 只改transform */
}
"opacity归A、transform归B" ------两条动画各管各的属性 ------从根本上避免交棒冲突。
本页球体用的是"严格对齐"法 ------因为入场动画本身也要改transform(scale + translateY),只能靠"对齐"解决。
能分工就分工,不能分工就严格对齐------这是双动画的通用原则。
技法3:一条光的故事
整页只有一个光源。
这是Hero区质感的核心。
所有发光 / 反光 / 阴影,全部指向同一个方向(左上)+ 同一个色系(青绿):
① 球体高光在左上
css
background: radial-gradient(circle at 33% 28%, #d8f7ec, #0d8574 48%, #073f37 82%);
/* ↑ 33% 28% = 左上方偏内 */
高光点在33% 28% ------左上偏内 ------光从"左上"打来。
② 球体投影在右下
css
box-shadow:
0 0 60px 10px rgba(13,133,116,.35), /* 外发光:同色系青绿 */
inset -14px -18px 40px rgba(0,0,0,.45); /* 内阴影:右下 */
外发光用青绿(同色系) ------"球在发光" 。
内阴影在右下(-14px -18px) ------"球被左上光照射"。
③ 极光带用同色系青绿
css
background: radial-gradient(ellipse, rgba(13,133,116,.4), transparent 65%);
和球体用的是同一个色号 ------"光源来自同一处"。
④ 地板网格也是青蓝
css
background: linear-gradient(rgba(125,211,252,.07) 1.5px, transparent 1.5px), ...
淡青蓝的网格线 ------和球体、极光、CTA都呼应。
⑤ CTA发光也是青绿
css
.cta { border: 1px solid rgba(92,201,180,.5); background: rgba(13,133,116,.18); }
.cta:hover { box-shadow: 0 0 30px rgba(13,133,116,.35); }
边框、背景、hover光晕 ------全是青绿同色系。
"观众不会总结这些细节,但会感到'这页很贵'。"
这就是"光照纪律"的含义 ------不是"每个元素都做得好看",而是"所有元素都遵守同一个光的规则"。
对比一下"光源打架"的页面:
- 球体光从左上,按钮光从右上
- 极光是紫色,CTA是红色
- 地板是冷青,标题是暖橙
结果:每个元素单独看都挺好看,合在一起**"像四个设计师做的"**。
这就是为什么"光照纪律"是Hero的质感命脉。
"Hero的质感 20%来自编排,80%来自光照纪律。"
四、踩坑位(我全踩过,你直接避)
① 有delay没写both
css
/* ❌ delay期间元素以"裸奔态"(opacity 1)站在台上,开场全穿帮 */
.kicker { animation: in .5s ease .1s; }
/* ✅ both:delay期间保持"第一帧"状态 */
.kicker { animation: in .5s ease .1s both; }
animation-fill-mode: both是入场动画的必备:
backwards:动画开始之前 (delay期间),保持第一帧的状态forwards:动画结束之后 ,保持最后一帧的状态
只写forwards ------delay期间元素显示"初始CSS状态" ------如果初始态是opacity: 1,开场所有元素都亮着 ------"裸奔"。
只写backwards ------动画结束后元素回到初始态 ------"闪回"。
写both ------两头都管 ------开场安全、结束稳妥。
这是"分镜表"动画的通用必备 ------打字机强调过forwards,今天多了backwards的场景。
② 两条动画都在动transform且交棒不齐
css
/* ❌ 入场结束时元素在translateY(20px),悬浮动画的from是0------1.6s 那刻会"跳" */
@keyframes in { to { transform: translateY(20px); } }
@keyframes bob { from { transform: translateY(0); } }
两条动画都在改transform ------后一条动画一旦运行,整个transform属性被接管------前一条动画的"终态"不再生效。
所以两端的值必须"逐值相等":
in的to:transform: none(等价于translateY(0) scale(1))bob的from:translateY(0)
两者对齐 ------交棒无缝。
更稳妥的做法是"属性分工":
css
/* ✅ 一条管opacity,一条管transform------永不冲突 */
.orb {
animation:
fade-in .8s ease .7s both, /* 只改opacity */
bob 5s ease-in-out 1.6s infinite alternate; /* 只改transform */
}
"能分工就分工,不能分工就对齐" ------双动画的通用原则。
③ 轨道环组合变换漏写rotateX
css
/* ❌ 环会从"倾斜轨道"瞬间弹回平面再转 */
@keyframes spinz1 {
from { transform: rotateZ(0deg); }
to { transform: rotateZ(360deg); }
}
/* ✅ 每一帧都写全组合变换 */
@keyframes spinz1 {
from { transform: rotateX(74deg) rotateZ(0deg); }
to { transform: rotateX(74deg) rotateZ(360deg); }
}
组合变换里,每个关键帧都要写全所有transform函数 ------不能只写"变化的那一个"。
为什么?
因为 transform每个关键帧是"整体替换" ------不是"增量叠加"。漏写rotateX ------这一帧的 transform里就没有rotateX ------环会瞬间"弹回平面" ------再转。
这是之前就强调过的"顺序 + 完整"原则------今天它换了个场景又出现一次。
④ 地板层挡点击
css
/* ❌ fixed装饰层挡住CTA,鼠标点不到按钮 */
.floor { position: fixed; /* ... */ }
/* ✅ 装饰层记得pointer-events: none */
.floor { position: fixed; pointer-events: none; }
position: fixed的装饰层 (极光带、地板网格、暗角)------它们覆盖整个视口 ------默认会拦截所有点击。
CTA按钮明明在"上面" ------却点不到------因为装饰层在事件上"截胡"了。
所有"纯视觉装饰层"都加pointer-events: none ------"可看不可摸"。
⑤ 入场做完就没事了
css
/* ❌ 入场2秒后,页面彻底"死"了 */
.kicker { animation: in .5s ease .1s both; } /* 只在0.1s ~ 0.6s动一下 */
h1 { animation: in .7s ease .35s both; } /* 只在0.35s ~ 1.05s动一下 */
/* ... 2 秒后全部静止 ... */
"入场是预告片,正片是页面常态。"
如果入场做完就静止 ------2秒后页面"死"了 ------像一张静态海报。
至少留一条低速常驻动画 ------让页面"活着"。
本页的常驻动画:
- 球体悬浮 (
bob 5s)------中速,气息感 - 轨道环自转 (
spinz1 12s/spinz2 9s)------中速,机械感 - 极光带漂移 (
drift 16s)------低速,氛围感
三条动画,三个档位,各司其职 ------这是讲过的"动画分档"心法。
注意错拍 :轨道环用12s / 9s(互质)------永不同步 ------像有生命。
"错拍才高级"
五、变式挑战(本周作业,10--30分钟,动手才有收获)
任务:换皮成你自己的Hero。
基础版(10分钟) ------替换三样东西:
| 位置 | 替换成 |
|---|---|
| 文案 | 你的博客slogan / 产品一句话 |
| 球体 | 你的头像圆形裁切 + 同款光晕 |
| 参数 | 你的三个数据(粉丝数 / 项目数 / 年份......) |
| CTA | 你的行动号召(订阅 / 联系 / 查看作品) |
挑战点:只准改内容与颜色token,不准动任何delay / 时长。
为什么这个限制很重要?
因为**"编排独立于内容"**------分镜表是"骨架",内容是"血肉"。
- 换内容 :骨架不动,血肉换新------省时省力
- 改骨架 :所有元素的时间要重新对齐------牵一发动全身
如果你能"不动delay就换出一个新Hero" ------说明你真正掌握了"编排"这件事。
进阶版(20--30分钟):
① 换色系
保留分镜表,把青绿换成你喜欢的色系:
- 琥珀金 (
#e8a95c / #b06a2a)------暖调,像烛光 - 赛博紫 (
#b9a7ff / #7a3bd0)------冷调,像霓虹 - 珊瑚红 (
#ff8a8a / #c94a4a)------活力,像海报
改色要"成套" ------球体高光、外发光、极光、网格、CTA一起换 ------不能只换一个。
② 加"hover反应"
CTA的hover效果可以升级:
css
.cta:hover {
background: rgba(13,133,116,.34);
box-shadow: 0 0 30px rgba(13,133,116,.35);
transform: translateY(-2px) scale(1.02); /* 微微浮起 + 放大 */
}
"浮起 + 放大"是按钮的通用交互语言 ------和磁吸导航一个思路。
③ 加"滚动后的状态"
复习滚动驱动 ------让Hero在滚动时淡出:
css
main {
animation: fade-out linear both;
animation-timeline: view();
animation-range: exit 0% exit 100%;
}
@keyframes fade-out { to { opacity: 0; transform: scale(.95); } }
滚动离开首屏时,Hero整体淡出 + 微微缩小 ------为下方内容让位。
④ 加"环境声"
省略------这会破坏"零JS"人设。
六、小结
今天这一篇里藏着 6个通用技能点:
| 技能 | 适用场景 |
|---|---|
| 先画分镜表,再写代码 | 所有"多元素入场"场景 |
| 相邻分镜间隔0.15 ~ 0.35s | 所有"编排节奏"的参考值 |
both保证delay 期间不"裸奔" |
所有带delay的入场动画 |
| 双动画"交棒对齐"或"属性分工" | 所有"入场 → 常驻"的动画 |
| 光照纪律(一条光的故事) | 所有Hero区 / 品牌页 |
| 入场后留常驻动画 | 所有"演示页 / 落地页" |
最值得记住的三条:
① 先画分镜表,再写第一行CSS。
分镜表是"设计稿" ------delay、时长、缓动、之后的状态 ------全部先想清楚。
然后写代码就是"翻译" ------一行不多、一行不少。
"想到哪写到哪" ------必然导致时间点混乱 ------这是编排型动画的头号反面教材。
② 双动画交棒:to必须等于下一棒的from。
"入场 → 常驻" ------所有Hero区都会遇到的两段式动画。
能分工就分工 ------opacity归A、transform归B。
不能分工就严格对齐 ------逐值逐像素核对。
③ "Hero的质感20% 来自编排,80%来自光照纪律。"
观众不会总结这些细节,但会感到"这页很贵"。
"这页很贵"的感觉,不来自"每个元素都做得好看" ------来自"所有元素遵守同一个光的规则"。
一条光的故事 ------这是Hero设计的核心审美纪律。
如果这一周对你有帮助,三连支持一下这个新专栏 🙏
点赞 = 更新加速,收藏 = 随时复制代码,评论 = 你想看什么效果,我排进日程。
本专栏所有demo,复制即用,随便商用。
目标更新:不断更。