先给结论:这个项目看着小,其实把前端地基里的几块硬骨头全踩了一遍——布局、定位、变形、动画、贝塞尔曲线、响应式,一样没落下。你花半小时把它手搓出来,收获比刷十道面试题实在。标题里写着“小白别慌”,是因为我当年学 CSS 动画时,真被各种 transform 和 keyframes 折磨得不轻,所以这篇文章把容易踩的坑全给你标出来了,照着写就行。
1. 项目概述与核心思路拆解
1.1 这个钟摆项目到底在练什么
先说清楚,这个钟摆不是那种用 JavaScript 定时器驱动、每帧改坐标的写法,而是纯 HTML + CSS 动画实现。核心只有两个:一个带 transform-origin 的旋转元素,一组 @keyframes 关键帧。你可能会觉得,这不就是个旋转动画吗?对,但恰恰是“旋转”这一点,把 CSS 里最容易混淆的几个概念全串起来了。
我在带新人时发现,很多人写 CSS 动画特别喜欢用 left、top 去移动元素,结果页面一卡一卡的,还说不清为什么。这个钟摆项目的第一个价值,就是逼你学会用 transform 来做位移动画,顺便把 transform-origin(旋转中心)搞明白——你只要把旋转中心设错,钟摆就会绕着自己中心转,而不是绕着顶部悬挂点转,那画面简直没法看。
第二个价值是贝塞尔曲线。钟摆的物理特性是:在最高点速度为零,在最低点速度最大,然后减速上升。这个先快后慢再减速的过程,恰恰对应 CSS 动画里的 ease-in-out 缓动函数。你做完以后会发现,动画顺不顺、真不真,80% 的功劳在缓动曲线,而不在动画本身。
第三个价值是布局。一个完整的钟摆页面需要表盘、刻度、摆杆、摆锤、悬挂点,它们之间有严格的层级关系。怎么用 flex 居中、怎么用绝对定位对齐旋转中心、怎么把刻度均匀分布在圆上,这些基本功全在这个项目里练到了。
1.2 为什么不用 JavaScript 而选纯 CSS
这个问题值得认真回答,因为面试时也常被问到。用 JS 实现无非是 requestAnimationFrame 里不断改角度,效果一样能出来,但纯 CSS 的优势很明显:浏览器对 transform 和 opacity 做了合成器优化,动画运行在 GPU 合成层上,不触发重排(reflow)和重绘(repaint),主线程完全不忙。你用 JS 去改 left 就会触发布局计算,性能差一个量级;改 transform 不会。
另外,纯 CSS 方案代码量少、不用管定时器生命周期、不用考虑组件卸载时清理监听器。对新手来说,CSS 方案的理解成本也低——你只需要声明“从 A 状态到 B 状态”,剩下的交给浏览器。JS 方案你得自己记录当前帧、自己算增量、自己处理边界条件,这属于把简单问题做复杂了。先学会用最简单的方式达成目标,再考虑更复杂的方案,这是我给新人的一贯建议。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动手前的关键概念:三个必须吃透的知识点
2.1 transform 和 transform-origin 的关系
transform 是 CSS 里最常用的变形属性之一,它支持 rotate()、translate()、scale()、skew() 等函数。它的特点是:把元素看作一个独立的坐标系,然后对这个坐标系进行整体变换。transform-origin 则是给这个变换指定一个基准点,默认是元素中心点(50% 50%),所以普通元素旋转时都是绕自己中心转。
钟摆的物理结构决定了它不能绕中心转,必须绕顶部悬挂点转。所以你要把 transform-origin 设为顶部中点,比如 top center,或者写成 50% 0%。这个值的理解直接决定整个项目的成败,我见过太多人卡在这一步:旋转中心不对,摆杆像风扇叶一样转圈,而不是左右摆动。
比如你的摆杆是一个长 400px、宽 10px 的元素,默认 transform: rotate(30deg) 会绕元素正中心旋转,而设置 transform-origin: top center 后会绕顶部中心旋转。钟摆就是后者。这个知识点不只是钟摆用,loading 旋转图标、仪表盘指针、旋转木马这些都是同一个原理。
2.2 animation 的完整写法与关键参数
CSS 动画写在 animation 属性里,完整格式是:
css复制animation: name duration timing-function delay iteration-count direction fill-mode;
钟摆用到的是:
css复制animation: swing 2s ease-in-out infinite alternate;
逐个解释:swing 是动画名,2s 是完整个周期的时间,ease-in-out 是缓动曲线,infinite 表示无限循环,alternate 是正反交替播放。这个 alternate 很关键——如果不加,动画会从摆动到右边后突然跳回左边再摆动,加了这个,它在播放完一轮后自动反向,逻辑上就像一个来回摆动。
还有一种写法是不用 alternate,而是在 @keyframes 里同时定义 0%、50%、100% 三个关键帧,让动画在两端点结束。两种都可以,我习惯用 alternate,因为省事、可读性好。
还要注意 fill-mode。如果动画不是无限循环,结束后元素会回到初始状态,这时可以设 forwards 让元素保持动画结束时的样子。钟摆是 infinite 的,所以无所谓,但你要复用这段代码做别的动画时,forwards 的坑总会遇到,提前知道没坏处。
2.3 贝塞尔曲线与缓动的直觉
CSS 的时间函数 ease-in-out 本质是一条三次贝塞尔曲线。它对应的标准值是 cubic-bezier(0.42, 0, 0.58, 1),含义是:开始慢,中间快,结束慢。对钟摆来说,这正好模拟了真实的物理运动:摆锤在最高点速度为零,往下加速,经过最低点速度最大,再往上减速到零。
如果你把缓动设成 linear,钟摆就会像个机器人一样匀速摆动,看起来非常假。设成 ease-in,摆从高位快速砸下来然后慢悠悠上去,也不自然。只有 ease-in-out 最接近钟摆的直觉感受。
想做更精细的物理模拟,可以把贝塞尔曲线调成 cubic-bezier(0.66, 0, 0.34, 1) 或者 cubic-bezier(0.45, 0, 0.55, 1),这些微调能让你感受曲线参数对动画质感的影向。我的建议是先跑默认值,再手动调一两个参数对比看,比看十篇理论文章都管用。
3. 完整实操:一步一步把钟摆做出来
3.1 搭建基础 HTML 结构
先建一个目录,里面放 index.html 和 style.css。HTML 结构不需要太复杂,关键是层级清晰。我的结构是:
html复制<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CSS 钟摆</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="clock">
<div class="pendulum-group">
<div class="pendulum-rod"></div>
<div class="pendulum-bob"></div>
</div>
<div class="center-cap"></div>
</div>
</body>
</html>
clock 是整体容器,用来做表盘和居中布局。pendulum-group 是挂在上面的旋转整体,里面的 pendulum-rod 是摆杆,pendulum-bob 是摆锤。center-cap 是悬挂点中间那个小圆盖,用来遮挡摆杆顶端和装饰。
为什么把摆杆和摆锤包在一个 div 里?因为这样只需要给 pendulum-group 设一个旋转动画,摆杆和摆锤就作为一个整体一起动,不用分别设动画、也不用担心不同步。你把 pendulum-group 理解成“一根手指”,施力点(旋转中心)在指尖,指着的那条线就是摆杆,指尖下面挂着的是摆锤。
3.2 CSS 核心样式与定位逻辑
先写容器和基础样式:
css复制* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(145deg, #1a1a2e, #16213e);
font-family: system-ui, sans-serif;
}
.clock {
position: relative;
width: 320px;
height: 320px;
border-radius: 50%;
background: #fffdf8;
box-shadow:
0 12px 24px rgba(0, 0, 0, 0.3),
inset 0 2px 6px rgba(255, 255, 255, 0.6);
}
body 用 flex 居中,这样表盘永远在页面正中间。clock 是一个 320px 的圆形,米白色背景,加了内外阴影让它看起来稍微有点立体感。
接下来是摆杆和摆锤的定位。这里有个关键点:pendulum-group 必须定在表盘中心偏上的位置,这样它的 transform-origin 才能对应到表盘的悬挂点。
css复制.pendulum-group {
position: absolute;
top: 60px;
left: 50%;
width: 12px;
height: 260px;
margin-left: -6px;
transform-origin: top center;
}
.pendulum-rod {
width: 6px;
height: 200px;
margin: 0 auto;
background: linear-gradient(to bottom, #8b7355, #5a4636);
border-radius: 4px;
}
.pendulum-bob {
width: 56px;
height: 56px;
margin: 8px auto 0;
border-radius: 50%;
background: radial-gradient(circle at 35% 35%, #f7c948, #d49b1e, #8a6a12);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.35);
}
pendulum-group 的左上角定位在(表盘水平中心,距顶 60px),用 margin-left: -6px 回移半个宽度,让元素的中心对准悬挂点。transform-origin: top center 保证旋转时绕着顶部的悬挂点转。表盘直径是 320px,顶部 60px 处大致是表盘的 12 点附近,刚好是挂钟悬杆的位置,这个数值不用太精确,视觉和谐就行。
摆杆我用渐变填充,让它上深下浅,更像金属杆。摆锤用径向渐变模拟金属反光,阴影可以增强立体感。这些都是加分项,不是核心,你可以先不加,后边再翻回来。
3.3 让钟摆动起来:关键帧与动画绑定
核心动画代码:
css复制@keyframes swing {
0% {
transform: rotate(30deg);
}
100% {
transform: rotate(-30deg);
}
}
.pendulum-group {
animation: swing 1.6s ease-in-out infinite alternate;
}
@keyframes swing 定义了两个关键状态:0% 时顺时针转 30 度,100% 时逆时针转 30 度。配合 alternate,动画就会从 +30° 摆到 -30°,再反向摆回 +30°,看起来就是连续的左右摆动。角度设多少看手感,20° 到 45° 都行,我最后调到了 30°,既明显又不夸张。
时间这里的 1.6s 和摆长有关系——物理上摆长越长周期越大。我这根摆杆 200px,加上摆锤,周期大概 1.5~2 秒,取 1.6s 视觉节奏刚好。你想让钟摆慢一点可以调到 2s,快一点就 1.2s,这个数值本身就是调优的过程。
到这里,一个最简陋的钟摆已经能动了。你打开浏览器就能看到一个摇摆的摆锤挂在圆形表盘里。接下来才是让项目从“能看”变成“好看”的部分。
3.4 加刻度、数字、中心盖,把页面做完整
一个光秃秃的表盘加个摆锤,看起来像半成品。我补上了表盘刻度、数字和悬挂点的小圆盖。
刻度盘的实现思路是:先用 flex 或绝对定位做出 12 个刻度,再逐一套上旋转角度。最常见的是把 12 个刻度放在一个容器里,每个刻度用 transform: rotate(角度) 定位。
我为了减少重复代码,用了一个更轻量的方式:先把刻度元素都堆在中心,然后用 transform-origin 加旋转把它们“甩”到圆周上。代码长这样:
html复制<div class="clock-face">
<span class="tick" style="--angle: 0deg;"></span>
<span class="tick" style="--angle: 30deg;"></span>
<span class="tick" style="--angle: 60deg;"></span>
<!-- 一直到 330deg -->
</div>
CSS 里用 CSS 变量统一控制:
css复制.clock-face {
position: absolute;
inset: 0;
border-radius: 50%;
}
.tick {
position: absolute;
top: 12px;
left: 50%;
width: 4px;
height: 18px;
margin-left: -2px;
background: #333;
border-radius: 2px;
transform-origin: bottom center;
transform: rotate(var(--angle)) translateY(146px);
}
这里的关键是 transform-origin: bottom center,让刻度底部对准表盘中心,然后 rotate(var(--angle)) 把刻度转到你想要的位置。再加上 translateY(146px) 往半径方向推出去,刻度就落在圆周上了。这个 146px 是表盘半径 160px 减去刻度长度 14px 左右的视觉微调,具体数值可以自己试。
数字“12、3、6、9”不用逐个定位,用同样的思路,在每个位置放一个绝对定位的 span,输入文本后 translateY 到对应半径,再微调字号。或者干脆用四个 position: absolute 配合 top/left 手写坐标,12 点、3 点、6 点、9 点就 4 个元素,手写很快。这里就用最简单的方式,毕竟重点是摆锤。
中心盖就是一个小圆:
css复制.center-cap {
position: absolute;
top: 60px;
left: 50%;
width: 18px;
height: 18px;
margin-left: -9px;
border-radius: 50%;
background: radial-gradient(circle at 35% 35%, #aaa, #444);
z-index: 2;
}
它盖在摆杆顶部和悬挂点之间,视觉上像一个真实的轴销,同时掩盖了摆杆顶部的切割边缘。z-index: 2 保证它在运动时始终盖在摆杆上面。
4. 进阶优化:响应式、层叠与性能
4.1 怎么适配小屏幕和不同窗口尺寸
320px 的表盘在手机上其实能放下,但笔记本、平板和大屏显示器上的观感完全不同。直接用固定像素会让它在某些场景下偏小或偏大。我建议用 min() 函数,把表盘尺寸设置成视口宽度的百分比加上一个下限:
css复制.clock {
width: min(320px, 80vw);
height: min(320px, 80vw);
}
但问题来了:表盘尺寸变了,刻度位移、摆锤尺寸、摆杆长度这些数值都是写死的,全盘都得跟着变。你可以把这些数值全部用 CSS 变量接管,然后把变量放进 :root 里定义,在媒体查询里改一套新值。这是一个完整的响应式思路:
css复制:root {
--clock-size: 320px;
--rod-length: 200px;
--bob-size: 56px;
--tick-radius: 146px;
}
@media (max-width: 480px) {
:root {
--clock-size: 240px;
--rod-length: 150px;
--bob-size: 42px;
--tick-radius: 106px;
}
}
然后把样式里的所有硬编码值替换为 var(--xxx)。这样一套 CSS 变量方案,手机、平板、桌面都能用同一套代码适配,后期调参也方便,不用满文件找魔法数字。
4.2 阴影、渐变和立体感
一个平面的圆盘加一条线,看着像设计稿而不是成品。加阴影是提升质感最直接的手段。我用了两层阴影:
css复制.clock {
box-shadow:
0 16px 32px rgba(0, 0, 0, 0.35),
inset 0 2px 8px rgba(255, 255, 255, 0.8);
}
外层阴影模拟表盘悬浮在背景上的投影,内层阴影模拟玻璃罩的高光。摆锤上也加了阴影和径向渐变,让它看起来像金属球而不是纯色圆形。背景用深蓝渐变,和米白色表盘形成对比,视觉层次就出来了。
如果你还想更进一步,可以在摆锤晃动时加一个动态投影:在表盘底部加一个椭圆形的伪元素,它的宽度随摆动方向变化。不过这个需要额外的 JS 控制或者用更复杂的 CSS 动画,属于进阶玩法,新手阶段不做也不会影响整体效果。
4.3 性能优化:合成层与 will-change
纯 CSS 动画虽然性能好,但也不是随便写写就完了。有一个细节值得你注意:给 transform 和 opacity 做动画时,浏览器会把这个元素提升到合成层。但如果元素数量多,每个都提升,反而会占太多内存,造成卡顿。所以不要给无关的元素加 will-change: transform,只给真正运动的元素加。
我这里的建议是给 .pendulum-group 加:
css复制.pendulum-group {
will-change: transform;
}
will-change 是告诉浏览器:“这个元素即将发生 transform 变化,提前给我放到合成层”。如果动画已经确知,加这一行能让动画更平滑。但注意,will-change 不能滥用,因为每个合成层都会占用 GPU 内存,你放 50 个带 will-change 的元素,页面肯定会卡。只在有明显动画的元素上加,然后动画结束后如果可以,最好移除——不过 infinite 动画不存在结束,所以这里加着没问题。
另外一点:动画过程中不要触发布局计算。比如不要在动画帧里修改 width、height、top、left、margin,这些属性一变就会触发 reflow,性能瞬间崩盘。在钟摆项目里,你动的是 transform,天然没问题。
5. 常见问题与避坑速查表
5.1 transform-origin 位置不对导致疯狂转圈
这是钟摆项目里最经典的坑。如果你发现自己写的钟摆不是来回摆,而是直接绕圈转,那不是动画逻辑错了,是 transform-origin 没设置,或者设成了默认的 50% 50%。默认情况下元素绕自己的正中心旋转,一个竖直的长条绕中心转,看起来就像一个风扇。
解决办法是在摆动容器上显式写好:
css复制transform-origin: top center;
如果你把 transform-origin 写在 @keyframes 内部,那也有坑。因为 transform-origin 不是动画属性,写进 keyframes 等于把它当成了动画目标,会让动画莫名其妙。正确写法是放在普通样式规则里。
5.2 动画不生效的几大原因
新手最常见的三个原因:动画名拼写不一致、没有写 animation-duration、元素被其他样式覆盖。第一个原因没什么好说的,检查拼写;第二个原因很容易忽略——animation 缩写里如果不写时间,浏览器用的默认值是 0s,动画瞬间完成,效果等于没做;第三个原因可能是你在 .pendulum-group 里用 position: static,元素就没法被绝对定位的子元素带住。
还有一个隐蔽的问题:如果你用了 display: none 的条件渲染,动画切换到显示时可能会闪一下。这个在多前端框架里经常出现,但纯 HTML 页面里不常见,知道就行。
5.3 怎么判断动画顺不顺:用 DevTools 看帧率
打开 Chrome DevTools,按 Ctrl+Shift+P,输入“Rendering”,打开“Frame Rendering Stats”或者“FPS meter”,就能看到当前页面的帧率。正常浏览器跑这个钟摆应该是 60fps 满帧,如果明显低于 60,说明有性能问题。排查时看 Performance 面板里是否有长时间的 “Rendering” 或 “Painting” 任务,有的话说明触发了太多重绘。
真遇到卡顿,先检查有没有给元素动态加阴影、有没有在用 filter 或者 backdrop-filter,这些样式在动画过程中每帧都会重算,性能开销很大。钟摆这种简单动画不该卡,如果卡了,大概率是页面里别的元素导致的。
5.4 兼容性:旧浏览器和前缀问题
transform 和 animation 在现代浏览器里支持都非常好,IE 已经退出历史舞台,不用再花太多心思。如果工作项目要求兼容旧浏览器,可以加 -webkit- 前缀:
css复制.pendulum-group {
-webkit-transform-origin: top center;
transform-origin: top center;
-webkit-animation: swing 1.6s ease-in-out infinite alternate;
animation: swing 1.6s ease-in-out infinite alternate;
}
不过现在用 Autoprefixer 自动加前缀更省事,手动写容易漏。对新项目新写法,我的建议是:默认支持最新版 Chrome、Safari、Firefox、Edge,除非需求明确要求兼容旧浏览器,否则不主动写前缀。
5.5 避坑指南:做动画时最容易犯的 6 个错误汇总
我做过的动画项目多了,发现新手高频踩坑就那么几个。整理成一个表方便你自查:
| 错误 | 表现 | 正确做法 |
|---|---|---|
| transform-origin 不设置 | 元素绕中心转 | 给运动容器显式设 top center |
| 动画名拼写不一致 | 根本没动画 | 统一 name,区分大小写 |
| animation-duration 为 0 | 动画瞬间完成看不见 | 缩写里一定写时间 |
| 用 top/left 做动画 | 页面卡顿、重排频繁 | 改用 transform: translate 或 rotate |
| 缓动设为 linear | 动画生硬不自然 | 用 ease-in-out 或自定义贝塞尔曲线 |
| 给无关元素加 will-change | 内存占用高、可能更卡 | 只给运动的元素加 |
5.6 代码审查:把最终完整代码过一遍
把全部代码合并,方便你直接复制运行。我加了必要的注释,保证你能看懂每段在干什么:
html复制<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CSS 钟摆</title>
<style>
:root {
--clock-size: min(320px, 80vw);
--rod-length: 200px;
--bob-size: 56px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(145deg, #1a1a2e, #16213e);
}
.clock {
position: relative;
width: var(--clock-size);
height: var(--clock-size);
border-radius: 50%;
background: #fffdf8;
box-shadow: 0 16px 32px rgba(0, 0, 0, 0.35), inset 0 2px 8px rgba(255, 255, 255, 0.8);
}
.pendulum-group {
position: absolute;
top: 60px;
left: 50%;
width: 12px;
height: 260px;
margin-left: -6px;
transform-origin: top center;
will-change: transform;
animation: swing 1.6s ease-in-out infinite alternate;
}
.pendulum-rod {
width: 6px;
height: var(--rod-length);
margin: 0 auto;
background: linear-gradient(to bottom, #8b7355, #5a4636);
border-radius: 4px;
}
.pendulum-bob {
width: var(--bob-size);
height: var(--bob-size);
margin: 8px auto 0;
border-radius: 50%;
background: radial-gradient(circle at 35% 35%, #f7c948, #d49b1e, #8a6a12);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.35);
}
.center-cap {
position: absolute;
top: 60px;
left: 50%;
width: 18px;
height: 18px;
margin-left: -9px;
border-radius: 50%;
background: radial-gradient(circle at 35% 35%, #aaa, #444);
z-index: 2;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
}
@keyframes swing {
0% {
transform: rotate(30deg);
}
100% {
transform: rotate(-30deg);
}
}
</style>
</head>
<body>
<div class="clock">
<div class="pendulum-group">
<div class="pendulum-rod"></div>
<div class="pendulum-bob"></div>
</div>
<div class="center-cap"></div>
</div>
</body>
</html>
这段代码是一个最小可用的成品。如果需要表盘刻度,把 3.4 里的刻度结构和样式加进去即可,不影响主体逻辑。
6. 项目复盘:钟摆背后真正值钱的东西
做完这个钟摆,你不只是会做一个钟摆。你掌握了三个可以复用到各种场景的技能:
第一个是“旋转中心思维”。loading 转圈、雷达扫描、仪表指针、进度环、轮播图指示器,全是 transform-origin 加 transform: rotate 的组合。你先想清楚旋转中心在哪,再想角度变化,这类动画基本就通吃了。
第二个是“用变换而不是用几何属性做动画”的思维。transform 不触发重排,top/left 触发,这个结论在你做复杂动画时特别关键。以后写任何动画,先问自己:可以拆解成 translate/rotate/scale 吗?能就别动 top 和 left。
第三个是“动画是修饰,结构才是主体”的思维。这个项目的 HTML 结构非常干净,三个 div 一个伪元素就说清楚了,所有细节都靠 CSS 叠加。先搭好结构,再往上面糊样式,是前端项目应该始终坚持的做法。
我在实际做这个项目时,最大的体会是:想让钟摆看起来“活”,不是靠动画更多,而是靠角度和缓动更准。30 度和 25 度的差别不仔细看看不出来,但 ease-in-out 和 linear 的差别一眼就能看出来。你多调几次参数,手感和审美就自然出来了。
最后分享一个小技巧:把 swing 角度调大一点,比如 45 度,然后把动画时间调到 2.4 秒,配上深色背景,你会看到摆锤在最低点时有一种饱满的冲力感,那种视觉张力比 30 度版本强很多。你可以试试不同组合,找到你自己喜欢的手感。这个项目后续还可以加的时间刻度、数字、闪烁的发光点、甚至一个用 JS 控制的真实时钟——但那是下一步的事了。先把这次练到的基本功,用在你自己想做的页面上。
