写在前面
昨天我们做了烟花------爆发、散开、熄灭,是"事件型"的动画。
今天换一种完全不同的气质:拟物(Skeuomorphism)。
在扁平化设计大行其道的今天,"拟物"听起来有点复古。但它恰恰是训练 CSS 功底的最佳题目------因为拟物有一个极其残酷的评判标准:
观众看一眼就知道"像不像"。
不像极光可以模糊、不像像素可以简化------怀表要是一眼假,就是假。
今天做一只黄铜怀表:
- 拉丝金属表壳------conic-gradient 转出高光
- 奶白表盘------罗马数字 + 双层刻度
- 黄铜指针 + 红色秒针------秒针每秒跳一格
- 玻璃高光------一条斜向白色渐变压在表盘上
- 整只表轻轻摆动------像挂在一只看不见的钩子上
拟物不是堆细节,是三件套的纪律。
一、先看效果

暗棕色皮革质感的空间里,一只黄铜怀表被吊在看不见的钩子上,缓缓左右摆动:
- 表壳:拉丝金属质感,随角度变换高光
- 表盘:奶白色,四角是12 / 3 / 6 / 9四个罗马数字,外圈一圈细密刻度
- 指针 :黄铜时针和分针指向 7点45分,红色秒针每秒跳一格
- 玻璃高光:从左上到右下,一条斜向白色渐变压在表盘上
- 表壳底部:一圈厚度阴影------表盘"有厚度"
- 地板:拖着一团模糊的影子
摆动周期4.5s,摆幅±2.4°------像真的怀表被风轻轻吹动。
怎么跑起来:
- 新建一个
demo.html - 把下面第二节的完整代码粘进去
- 双击用浏览器打开
📸 截图党提示 :录5秒GIF 抓"摆动 + 秒针跳格"最生动。静态截图选表身向左倾斜的时刻------那个角度的高光最亮、拉丝质感最明显。
二、完整代码(复制即用)
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 18 · 会摆动的拟物怀表</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
html, body { height:100%; }
body { display:flex; flex-direction:column; align-items:center; justify-content:center;
overflow:hidden; background:radial-gradient(ellipse at 50% 28%, #2a2119, #17110c 78%);
font-family:"SF Mono", Consolas, monospace; }
.wrap { display:flex; flex-direction:column; align-items:center; }
/* ------ 吊挂摆动:支点在体外(transform-origin 的 y 是负值)------ */
.swing { transform-origin:50% -34px; animation: swing 4.5s ease-in-out infinite alternate; }
@keyframes swing { from { transform:rotate(-2.4deg); } to { transform:rotate(2.4deg); } }
.ring { width:30px; height:36px; margin:0 auto -8px; border-radius:50%;
border:8px solid; border-color:#a8853f #6e5427 #a8853f #6e5427; }
.crown { width:22px; height:20px; margin:0 auto -6px; position:relative; z-index:2;
background:linear-gradient(90deg, #6e5427, #e8cf9a 45%, #6e5427);
border-radius:6px 6px 3px 3px; box-shadow:0 3px 6px rgba(0,0,0,.4); }
/* ------ 黄铜表壳:conic 拉丝 + 双向 inset ------ */
.case { width:250px; height:250px; border-radius:50%; padding:13px;
background:conic-gradient(from 210deg, #6e5427, #e8cf9a 12%, #8a6d3b 26%, #caa96a 42%,
#6e5427 55%, #e8cf9a 68%, #8a6d3b 80%, #caa96a 92%, #6e5427);
box-shadow:0 26px 46px rgba(0,0,0,.55),
inset 0 2px 6px rgba(255,236,190,.55), inset 0 -4px 10px rgba(0,0,0,.5); }
.dial { position:relative; width:100%; height:100%; border-radius:50%;
background:radial-gradient(circle at 50% 36%, #f8f0dd, #ead8b6 68%, #d3c09a);
box-shadow:inset 0 5px 16px rgba(90,70,35,.4), inset 0 -3px 8px rgba(255,255,255,.55); }
.dial::before { content:""; position:absolute; inset:12px; border-radius:50%;
background:
repeating-conic-gradient(from -1.4deg, rgba(96,74,38,.55) 0deg 2.6deg, transparent 2.6deg 90deg),
repeating-conic-gradient(from -0.5deg, rgba(96,74,38,.3) 0deg 1deg, transparent 1deg 30deg);
-webkit-mask:radial-gradient(closest-side, transparent 84%, #000 85%, #000 96%, transparent 96%);
mask:radial-gradient(closest-side, transparent 84%, #000 85%, #000 96%, transparent 96%); }
.dial::after { content:""; position:absolute; inset:0; border-radius:50%;
background:linear-gradient(135deg, rgba(255,255,255,.42), rgba(255,255,255,0) 42%);
pointer-events:none; }
.n { position:absolute; font-family:Georgia, "Times New Roman", serif;
font-size:20px; color:#6b5427; }
.n12 { top:13px; left:50%; transform:translateX(-50%); }
.n3 { right:17px; top:50%; transform:translateY(-50%); }
.n6 { bottom:11px; left:50%; transform:translateX(-50%); }
.n9 { left:17px; top:50%; transform:translateY(-50%); }
.brand { position:absolute; top:50px; width:100%; text-align:center;
font-size:8px; letter-spacing:.34em; color:#96784a; }
/* ------ 指针:金属渐变 + 负 delay 对时到 7:45:18 ------ */
.hand { position:absolute; left:50%; bottom:50%; transform-origin:50% 100%;
border-radius:5px; animation:rot linear infinite; }
.hour { width:8px; height:50px; margin-left:-4px;
background:linear-gradient(90deg, #8a6d3b, #4a3717 60%, #2e2210);
box-shadow:1px 2px 4px rgba(60,40,10,.35);
animation-duration:43200s; animation-delay:-27900s; }
.min { width:5px; height:74px; margin-left:-2.5px;
background:linear-gradient(90deg, #a8853f, #4a3717 60%, #2e2210);
box-shadow:1px 2px 4px rgba(60,40,10,.35);
animation-duration:3600s; animation-delay:-2700s; }
.sec { width:2px; height:86px; margin-left:-1px; background:#b3402e;
animation-duration:60s; animation-delay:-18s; animation-timing-function:steps(60); }
.sec::after { content:""; position:absolute; bottom:-11px; left:50%; width:8px; height:11px;
margin-left:-4px; background:#b3402e; border-radius:4px; }
.cap { position:absolute; left:50%; bottom:50%; width:15px; height:15px; margin:-7.5px;
border-radius:50%; background:radial-gradient(circle at 35% 30%, #f0d9a0, #8a6d3b 70%);
box-shadow:0 1px 3px rgba(0,0,0,.4); }
@keyframes rot { to { transform:rotate(360deg); } }
.floor { width:210px; height:18px; margin-top:36px; border-radius:50%;
background:radial-gradient(ellipse, rgba(0,0,0,.5), transparent 70%); }
.tag { position:fixed; right:18px; bottom:14px; font-size:10px;
letter-spacing:.28em; color:rgba(240,217,160,.3); }
</style>
</head>
<body>
<div class="wrap">
<div class="swing">
<div class="ring"></div>
<div class="crown"></div>
<div class="case">
<div class="dial">
<span class="n n12">12</span><span class="n n3">3</span>
<span class="n n6">6</span><span class="n n9">9</span>
<span class="brand">CHRONO · 18</span>
<div class="hand hour"></div>
<div class="hand min"></div>
<div class="hand sec"></div>
<div class="cap"></div>
</div>
</div>
</div>
<div class="floor"></div>
</div>
<div class="tag">每天一个惊艳 CSS · DAY 18</div>
</body>
</html>
三、三个核心技法拆解
代码看着长,其实核心只有三个点------每一个拆开都能用在别的地方。
技法1:conic-gradient拉「拉丝金属」
金属感的来源,你想一下------
不是"颜色多",而是------
高光角度连续变化。
真实的金属表面反射环境光:光从不同角度打过来,亮度一路渐变 。所以金属的关键不是"用金色",而是把高光做出"角度连续性"。
css
.case {
background: conic-gradient(from 210deg,
#6e5427, /* 暗铜 */
#e8cf9a 12%, /* 亮金 ------ 高光 */
#8a6d3b 26%, /* 中间棕 */
#caa96a 42%, /* 中间金 */
#6e5427 55%, /* 暗铜 ------ 阴影 */
#e8cf9a 68%, /* 亮金 ------ 又一圈高光 */
#8a6d3b 80%, /* ... */
#caa96a 92%,
#6e5427);
}
这就是"拉丝金属"的核心。
conic-gradient从210°起手 ------在四档色之间反复过渡。
#6e5427:暗铜(阴影面)#8a6d3b:中间棕#caa96a:中间金#e8cf9a:亮金(高光面)
四档色循环两轮 ------就像金属表面反射出一圈环境光。
为什么"循环两轮"?
只循环一轮:金属只有"一明一暗"------太干净、太假。
循环两轮 :金属有两组高光 + 两组阴影------更像真的黄铜表面(有凹凸、有反射)。
"从210°起手"是什么意思?
from 210deg让渐变的起点 在210°方向(左上角)------和"光源在左上"的设定一致。
**这还不是全部。**还要加两条inset阴影------给壳体厚度:
css
box-shadow:
0 26px 46px rgba(0,0,0,.55), /* 外投影:表壳悬空的落影 */
inset 0 2px 6px rgba(255,236,190,.55), /* 内高光:上边缘亮 */
inset 0 -4px 10px rgba(0,0,0,.5); /* 内阴影:下边缘暗 */
逐条读:
| 阴影 | 作用 |
|---|---|
0 26px 46px ... |
表壳悬浮的落影------底部深、远处淡 |
inset 0 2px 6px ... |
内嵌高光------上边缘亮 |
inset 0 -4px 10px ... |
内嵌阴影------下边缘暗 |
这三条阴影给"厚度"。
"上亮下暗"是拟物最重要的光向------光源在左上,所以:
- 上边缘:受光,亮
- 下边缘:背光,暗
材质立刻从"色块"变成"物件"。
技法2:催眠摆的支点在体外
摆动动画本身极其简单:
css
@keyframes swing { from { transform:rotate(-2.4deg); } to { transform:rotate(2.4deg); } }
.swing { animation: swing 4.5s ease-in-out infinite alternate; }
但真正让"摆动像真的"的,是transform-origin:
css
.swing { transform-origin: 50% -34px; }
transform-origin: 50% -34px是什么?
50%:水平居中-34px:垂直方向在元素上方34px处 ------一个在元素外部的点
这个点在DOM里不存在 ------但浏览器以它为支点旋转元素。
想象一下真实的怀表吊挂:
- 表挂在钩子上
- 表会在钩子下方绕钩子摆动
- 表本身不会"原地左右扭"
所以支点必须在"表体外部的上方" ------就是表链悬挂的那个点。
如果transform-origin写在元素中心会怎样?
css
/* ❌ 默认值center center:表原地左右扭,不像被吊着 */
.swing { transform-origin: center; }
结果 :整块表原地左右扭------像被一根钉子钉在墙上还在摇。
这是"吊挂"和"摇头"的本质区别。
摆幅压到±2.4°、周期4.5s------这是"悬挂物"而非"钟摆"的可信范围:
- ±2.4°:小幅度轻晃,像微风吹动
- 4.5s一个来回:慢节奏,符合真实吊挂物的"催眠感"
ease-in-out:两头慢中间快,像物理摆动的自然加减速alternate:往复循环,不用写两个关键帧
这是一条通用心法:
所有"吊挂 / 飘动 / 摆动"的动画,
transform-origin必须放在身体外部的"支点"处。
它适用于:吊灯、挂钟、铃铛、风吹的树叶、天平的托盘。
技法3:拟物三件套纪律
拟物最容易翻车的地方------不是"细节不够",而是"光源打架"。
本页所有拟物元素,共用同一套"光":
① 双向阴影------左上亮、右下暗
css
/* 表壳 */
box-shadow: 0 26px 46px rgba(0,0,0,.55), /* 外投影 */
inset 0 2px 6px rgba(255,236,190,.55), /* 上亮 */
inset 0 -4px 10px rgba(0,0,0,.5); /* 下暗 */
/* 表盘 */
box-shadow: inset 0 5px 16px rgba(90,70,35,.4), /* 上暗(凹陷) */
inset 0 -3px 8px rgba(255,255,255,.55); /* 下亮(凹陷反光) */
/* 表冠 */
box-shadow: 0 3px 6px rgba(0,0,0,.4); /* 下方投影 */
/* 秒针配重 */
/* 指针 */
box-shadow: 1px 2px 4px rgba(60,40,10,.35); /* 右下投影 */
所有阴影都是"左上亮、右下暗" ------光源唯一。
② 左上135°定向高光
css
/* 表壳高光:conic-gradient from 210deg,起点在左上 */
/* 表盘玻璃高光:135deg斜向渐变 */
.dial::after {
background: linear-gradient(135deg, rgba(255,255,255,.42), rgba(255,255,255,0) 42%);
}
/* 表冠圆柱高光:90deg横向渐变,中间亮 */
.crown {
background: linear-gradient(90deg, #6e5427, #e8cf9a 45%, #6e5427);
}
/* 指针金属高光:左侧1/3处亮 */
.hour {
background: linear-gradient(90deg, #8a6d3b, #4a3717 60%, #2e2210);
}
所有高光都从"左上"来 ------或横向、或斜向、或放射 ,但方向一致。
③ 材质色不超过三档
css
/* ✅ 只有四档色:深铜、中棕、中金、亮金 */
/* 所有金属元素共用这四档色 */
为什么是三档(本例四档)?
因为金属反光的色阶是有限的 ------暗、中、亮三档就够。
如果贪心加更多中间色(比如再加五种不同的棕色)------画面立刻"出土文物"。
拟物翻车的第二大原因:色阶贪多。
"光源唯一,材质才统一"------拟物翻车九成是光源打架。
如果你发现自己做的拟物"看起来不像"------第一件事不是加细节,而是检查"是不是所有阴影、高光都来自同一个光源"。
四、踩坑位(我全踩过,你直接避)
① 想用border-image: conic-gradient(...)做金属边框
css
/* ❌ border-image与border-radius不同圆,圆角处直接断裂 */
.case {
border: 13px solid;
border-image: conic-gradient(...) 1;
border-radius: 50%;
}
border-image和border-radius是"不兼容"的 ------border-image会被切成四段方形的边框图案,遇到圆角时会断裂。
正确做法:嵌套法。
html
<div class="case"> <!-- 外层conic圆(金属壳) -->
<div class="dial">...</div> <!-- 内层表盘 -->
</div>
css
/* ✅ 外层conic圆 + padding + 内层dial */
.case {
width: 250px; height: 250px; border-radius: 50%;
padding: 13px; /* 让出13px的"边框" */
background: conic-gradient(...); /* 金属壳 */
}
.dial {
width: 100%; height: 100%; border-radius: 50%;
background: radial-gradient(...); /* 奶白表盘 */
}
外层圆和内层圆各自是独立的圆 ------圆角不会断裂。
"圆环金属壳 = 外层conic圆 + padding + 内层dial"------这是所有"复杂圆环"的通用做法。
② 摆动的origin写在元素中心
css
/* ❌ 默认center center:整块表原地左右扭,不像被吊着 */
.swing { transform-origin: center; }
/* ✅ 支点悬在体外:50% -34px */
.swing { transform-origin: 50% -34px; }
这是"吊挂"和"摇头"的分界线。
判断法 :想象有一个钩子 ------你的transform-origin应该正好在钩子的位置 。如果钩子在表体外部,y就是负值。
摆幅小一点(±2.4°),周期长一点(4.5s)------"轻轻摆动"比"大幅摇摆"更像真的吊挂。
③ 拟物色超过三档
css
/* ❌ 金银铜再加一堆中间色,画面立刻"出土文物" */
background: linear-gradient(90deg,
#4a3717, #6e5427, #8a6d3b, #a8853f, #caa96a, #e8cf9a, #f0d9a0, #fff0c0);
拟物最容易"贪多"------总觉得多一个色阶就更真实。
实际上相反:
- 色阶少(3~4档):干净、有质感
- 色阶多(7档以上):糊成一团、显得"脏"
锁定"一深、一浅、一亮"三档 (深铜、中金、亮金),其余全靠透明度和白黑云雾。
④ 秒针没配重
css
/* ❌ 细长秒针绕针尖转,视觉重心不稳 */
.sec { width: 2px; height: 86px; background: #b3402e; }
/* ✅ 针尾加一小段同色矩形当配重 */
.sec::after {
content:""; position: absolute; bottom: -11px; left: 50%; width: 8px; height: 11px;
margin-left: -4px; background: #b3402e; border-radius: 4px;
}
真实的机械表,秒针有两段:
- 长段:从中心指到表盘外圈
- 短段 :从中心往后伸出一小截,当配重
少了这一小截 ------秒针看起来"头重脚轻",像要飞出去。
加这一小截 ------机械表的味道就对了。
这条细节不只适用于秒针 ------所有"指针 / 指针状图形 / 风向标 / 指南针",尾部都该加配重。
⑤ 玻璃高光盖过指针
css
/* ❌ 高光层放在指针前面,指针被盖住看不清 */
.dial::before { /* 高光 */ }
/* ✅ 高光层放最后(DOM 顺序在指针之后),透明度压在0.4以下 */
.dial::after {
background: linear-gradient(135deg, rgba(255,255,255,.42), rgba(255,255,255,0) 42%);
/* ↑ 42%透明度,别再高了 */
pointer-events: none;
}
"玻璃存在感要有,指针必须看得清。"
顺序 :指针在DOM里放前面,玻璃高光在指针后面 (::after)------玻璃"压"在指针上,但不遮住。
透明度控制在0.4以下 ------高光太强,指针就"雾化"了。
这是"拟物"的通用心法 ------所有"装饰层压在主图层上"的场景,透明度都是控制在0.3 ~ 0.4之间。
五、变式挑战(10--30分钟,动手才有收获)
任务:改成倒计时怀表。
核心改动:
css
/* 秒针动画周期从60s改600s(10分钟一圈) */
.sec { animation-duration: 600s; }
/* delay调到表盘读数就是你想要的倒计时 */
.sec { animation-delay: -XXXs; } /* XXX = 已流逝秒数 */
"读数即倒计时"的心算方法:
- 负delay = 已流逝时间
- 想让它显示"还剩3分钟" :让指针指向"3分钟"位置------delay = -(10 - 3)× 60 = -420s
- 想让倒计时从"还剩5分钟"开始:delay = -300s
再加上一条conic进度环:
css
.case::before {
content:""; position: absolute; inset: -8px; /* 外扩8px,套在表壳外 */
border-radius: 50%;
background: conic-gradient(#b3402e 0 30%, rgba(255,255,255,.08) 30% 100%);
mask: radial-gradient(closest-side, transparent 92%, #000 93%);
animation: spin 600s linear infinite; /* 和秒针同步 */
}
效果 :表壳外围有一圈红色的"进度",随着时间推移慢慢填满。
难点:
- "读数即倒计时"的心算------delay负值对应"已流逝",想清楚"还剩多少 → 已流逝多少"
- 进度环和秒针同步------两者动画时长必须一致(都是600s),否则会不同步
- 颜色随进度变化------用两层conic叠出来(红色表示"已过去",深灰表示"还剩")
其他变式方向(选做):
① 加一条秒针的"走路声"
省略------这需要JS或音频文件,会破坏"零JS"人设。
② 让表盘有"指针移动时的阴影"
给指针加一个"双层阴影"------指针转到某些角度时,阴影方向跟着变。这是拟物的高级玩法,但CSS做不到"方向随角度变"------需要JS实时计算。
③ 换怀表为正计时
秒针、分针、时针从00:00:00开始------去掉负delay即可。
④ 加"表盘反光"的动态效果
让玻璃高光的透明度缓慢呼吸------模拟光线在玻璃上的微微变化。
六、小结
今天这一篇里藏着 5个通用技能点:
| 技能 | 适用场景 |
|---|---|
conic-gradient做"拉丝金属" |
所有金属质感(表壳、按钮、边框、奖章) |
transform-origin放在体外做吊挂 |
所有"吊挂 / 摆动 / 飘动"动画 |
| 拟物三件套纪律(光、向、色) | 所有拟物设计 |
inset阴影做厚度 |
所有"有厚度的圆环 / 按钮 / 卡片" |
| 嵌套法做复杂圆环 | 所有"外圈 + 内圈"的圆环结构 |
最值得记住的三条:
① 金属感不来自"颜色多",来自"高光角度连续变化"。
conic-gradient是最好的工具------它让高光沿着"角度"连续变化,而不是"沿着直线渐变"。
四档色循环两轮------就是"拉丝金属"的全部秘密。
② 吊挂物的支点,必须在身体外部。
transform-origin: 50% -34px------这个负值,就是"看不见的钩子"的位置。
"吊挂"和"原地摇头"的区别,就在这一个数字。
③ 拟物翻车九成是"光源打架"。
光源唯一------所有阴影都来自同一个方向。
光向一致------所有高光都从"左上"来。
色阶少而精------三档就够,多了就"出土文物"。
这三条纪律,是拟物和"堆细节"的分界线。
如果这一篇对你有帮助,三连支持一下这个专栏 🙏
点赞 = 更新加速,收藏 = 随时复制代码,评论 = 你想看什么效果,我排进日程。
本专栏所有demo,复制即用,随便商用。
目标更新:100 天,不断更。