写在前面
昨天我们做了一只拟物怀表------金属、皮革、厚度,都是"实体"的质感。
今天做两个"笔画级"的文字效果:
- 流光文字------标题里一道光带缓慢扫过,像金属或玻璃在反光
- 手写签名------签名像被"画"出来一样,一笔一笔出现、停笔、再被擦掉、循环
看起来是两个不相干的效果,其实本质完全一样:
都是"让一段渐变 / 线条,按进度展示"。
- 流光:让"一段高光渐变"按进度扫过
- 签名:让"一条SVG路径"按进度画出来
理解了这个统一视角,你就能把这两个效果都拆到最简单。
一、先看效果

深色卡片上,从上到下三件事:
- 顶部 :衬线大字「AURUM」------金属灰字面里,一道蓝白光带每4.2秒从右往左扫过
- 中部 :一条蓝色曲线 凭空起笔,游走转折像在签某个花体名字;收笔后一道绿色下划线补上
- 底部 :停两秒,笔画像被橡皮擦反向擦掉,再重新写起
签名的完整循环约6.5秒------写 → 停 → 擦 → 再写。
怎么跑起来:
- 新建一个
demo.html - 把下面第二节的完整代码粘进去
- 双击用浏览器打开
📸 截图党提示 :录7秒GIF 抓签名完整循环最生动------静态图看不出"书写"这个过程。如果只能截图,选签名写到2/3的瞬间------笔画悬在半空,最能暗示"正在写"。
二、完整代码(复制即用)
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 19 · 流光文字与手写签名</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; overflow:hidden;
background:#10151c; font-family:"PingFang SC", sans-serif; }
.card { width:min(560px, 92vw); padding:52px 48px 44px; border-radius:18px;
background:#131a23; border:1px solid rgba(255,255,255,.07);
box-shadow:0 30px 70px rgba(0,0,0,.45); text-align:center; }
.label { font:400 10px/1 "SF Mono", Consolas, monospace;
letter-spacing:.34em; color:#5b6878; }
/* ------ 流光文字:渐变裁进字形,高光带横扫 ------ */
.shine { margin-top:30px;
font-family:Georgia, "Times New Roman", serif;
font-size:64px; font-weight:600; letter-spacing:.18em;
background:linear-gradient(110deg, #5a6472 0%, #5a6472 38%, #f2f6fb 47%, #9fd6ff 53%, #5a6472 62%, #5a6472 100%);
background-size:250% 100%;
-webkit-background-clip:text; background-clip:text; color:transparent;
-webkit-text-fill-color:transparent;
animation:sheen 4.2s linear infinite; }
@keyframes sheen { from { background-position:250% 0; } to { background-position:-250% 0; } }
.divider { width:64px; height:1px; background:rgba(255,255,255,.12); margin:30px auto 6px; }
/* ------ 手写签名:SVG stroke 描边,写完停笔、再被擦掉、循环 ------ */
.sig { display:block; width:min(420px, 78vw); margin:0 auto; overflow:visible; }
.stroke { fill:none; stroke-linecap:round; stroke-linejoin:round; }
.s1 { stroke:#9fd6ff; stroke-width:3.5; stroke-dasharray:900; stroke-dashoffset:900;
animation: draw1 6.5s ease-in-out infinite; }
.s2 { stroke:#5cc9b4; stroke-width:3; stroke-dasharray:420; stroke-dashoffset:420;
animation: draw2 6.5s ease-in-out infinite; }
@keyframes draw1 { 0% { stroke-dashoffset:900; } 30% { stroke-dashoffset:0; }
72% { stroke-dashoffset:0; } 100% { stroke-dashoffset:-900; } }
@keyframes draw2 { 0%, 16% { stroke-dashoffset:420; } 42% { stroke-dashoffset:0; }
74% { stroke-dashoffset:0; } 100% { stroke-dashoffset:-420; } }
.note { margin-top:26px; font:400 10px/1 "SF Mono", Consolas, monospace;
letter-spacing:.3em; color:#39434f; }
.tag { position:fixed; right:18px; bottom:14px; font:400 10px/1 "SF Mono", Consolas, monospace;
letter-spacing:.28em; color:rgba(230,237,243,.22); }
</style>
</head>
<body>
<div class="card">
<div class="label">SIGNATURE STUDY · DAY 19</div>
<div class="shine">AURUM</div>
<div class="divider"></div>
<svg class="sig" viewBox="0 0 420 140">
<path class="stroke s1" d="M20 96 C 60 20, 92 18, 96 62 C 99 92, 76 108, 62 100 C 50 93, 66 74, 96 72 C 150 68, 168 40, 196 44 C 226 48, 214 88, 246 84 C 282 79, 288 46, 318 52 C 344 57, 340 84, 368 78 C 386 74, 396 60, 404 48"/>
<path class="stroke s2" d="M26 116 C 120 130, 260 126, 398 104"/>
</svg>
<div class="note">BACKGROUND-CLIP:TEXT + STROKE-DASHOFFSET</div>
</div>
<div class="tag">每天一个惊艳 CSS · DAY 19</div>
</body>
</html>
三、三个核心技法拆解
代码不长,但每一个技法都值得单独拿出来讲------它们不是"这个效果专属",而是通用工具。
技法1:background-clip: text把渐变「灌进」字形
流光文字的核心思路,你想一下------
不是"文字在发光",而是------
"一道光带扫过文字的表面**。"**
所以本质是"渐变在动",文字只是"渐变的容器"。
css
.shine {
/* ① 文字本身的颜色 → 透明 */
color: transparent;
-webkit-text-fill-color: transparent;
/* ② 背景是一段"三段式"渐变 */
background: linear-gradient(110deg,
#5a6472 0%, /* 基色 */
#5a6472 38%, /* 基色 */
#f2f6fb 47%, /* 高光带 ------ 从暗到极亮 */
#9fd6ff 53%, /* 高光带 ------ 从极亮到蓝 */
#5a6472 62%, /* 基色 */
#5a6472 100%); /* 基色 */
/* ③ 背景放大到250%,光带才有空间滑动 */
background-size: 250% 100%;
/* ④ 关键:把背景"裁剪"到文字的笔画里 */
-webkit-background-clip: text;
background-clip: text;
/* ⑤ 动画只扫background-position */
animation: sheen 4.2s linear infinite;
}
@keyframes sheen {
from { background-position: 250% 0; }
to { background-position: -250% 0; }
}
逐条理解:
① 文字颜色透明------这是"让背景透出来"的前提
css
color: transparent; /* 通用 */
-webkit-text-fill-color: transparent; /* Safari / 老Chrome需要 */
两行都要写 ------这是 Safari 的历史遗留。只写color: transparent在Safari里会"露底"(显示出默认的黑色文字)。
② background-clip: text ------ 让背景只从字形里透出来
这是整个效果的核心属性 ------它把"元素的矩形背景"裁成"文字笔画的形状"。
结果 :渐变不再覆盖整块矩形,而是只出现在文字的笔画里。
③ background-size: 250% 100% ------ 给光带留出"滑动空间"
默认background-size: 100% ------渐变正好铺满元素,没有"藏起来的部分",也就没法扫。
放大到250% ------渐变比元素宽2.5倍 ------光带可以从外面扫进来、再扫出去。
④ 动画只扫background-position
css
@keyframes sheen {
from { background-position: 250% 0; } /* 光带在"很远的右边" */
to { background-position: -250% 0; } /* 光带扫到"很远的左边" */
}
从250%扫到-250% ------光带完整地穿过文字,从头到尾扫一遍。
为什么首尾都停在基色#5a6472?
因为渐变的0% ~ 38%和62% ~ 100%都是基色------所以:
- 光带在起点(250%)时------可见区域是基色
- 光带在终点(-250%)时------可见区域还是基色
- 循环重置时,画面上都是"纯基色" ------无缝衔接
这是"无缝循环"的通用心法 ------让循环的首尾两端,视觉上完全一致。
技法2:stroke-dashoffset是「笔尖进度条」
手写签名的核心,是把"画出来的线"变成"一段进度"。
怎么理解?
把SVG路径想象成一条"虚线":
css
.stroke { stroke-dasharray: 900; stroke-dashoffset: 900; }
stroke-dasharray: 900 ------ 意思是"这条线由900px实线 + 900px空白交替组成"。
因为路径总长度就是900左右 ------所以实际上整条路径就是一段900px的实线。
stroke-dashoffset: 900 ------ 意思是"虚线整体向后偏移900px"。
结果 :偏移900px之后,原来那条 900px 的实线完全移出了可见范围 ------路径上什么都不显示。
动画把offset从900推到0:
css
@keyframes draw1 {
0% { stroke-dashoffset: 900; } /* 完全隐藏 */
30% { stroke-dashoffset: 0; } /* 完全画出 */
72% { stroke-dashoffset: 0; } /* 停笔 */
100% { stroke-dashoffset: -900; } /* 反向擦掉 */
}
逐段读:
| 分段 | offset | 视觉 |
|---|---|---|
| 0% → 30% | 900 → 0 |
线条被"画"出来------笔尖从左往右走 |
| 30% → 72% | 0 → 0 |
停笔------签名完整显示 |
| 72% → 100% | 0 → -900 |
线条被"擦"掉------从起笔端反向消失 |
关键:offset推到负值(-900)------线条从起笔端开始"反向消失"。
为什么是"反向"?
因为offset是沿路径方向的偏移:
- 正向偏移(正值) :实线段向后推 ------末端的线先消失
- 负向偏移(负值) :实线段向前拉 ------起笔端的线先消失
"起笔端先消失"看起来就像被"橡皮擦擦掉"------从线的起点开始,一路往回擦。
"写、停、擦"三幕剧,一个属性全包。
这是stroke-dashoffset的通用价值 ------所有"沿线描边"的效果(进度条、路径动画、勾选对号、曲线绘制)都靠它。
技法3:双路径 = 双笔顺
"签名"看起来是"一笔写成",但代码里其实是"两条独立路径"。
html
<svg viewBox="0 0 420 140">
<path class="stroke s1" d="M20 96 C 60 20, ..."/> <!-- 主体:花体字 -->
<path class="stroke s2" d="M26 116 C 120 130, ..."/> <!-- 下划线 -->
</svg>
两条路径各自持有动画:
css
/* 主体:0% ~ 30%写完,72% ~ 100%擦掉 */
@keyframes draw1 {
0% { stroke-dashoffset: 900; }
30% { stroke-dashoffset: 0; }
72% { stroke-dashoffset: 0; }
100% { stroke-dashoffset: -900; }
}
/* 下划线:16%才起笔,42%写完,74%起擦 */
@keyframes draw2 {
0%, 16% { stroke-dashoffset: 420; } /* 前16%保持隐藏 */
42% { stroke-dashoffset: 0; } /* 42%写完 */
74% { stroke-dashoffset: 0; } /* 停笔到74% */
100% { stroke-dashoffset: -420; } /* 擦掉 */
}
时间轴对齐:
0s 1.95s 3.0s 4.7s 6.5s
│ │ │ │ │
├──主体写下─┤ │ │ │
├──主体停笔──┤ │ │
├─下划线写下─┤ │ │ │
├──下划线停笔┤ │
├──一起擦──┤
"笔顺感"的三个要点:
① 下划线的起笔比主体晚 16%------主体写了一小半,下划线才"跟上"。
为什么不是"等主体写完再画下划线"?
因为真实签名里,人是"边写边补充"的 ------主体签名写到一半,手就已经顺势拉出一道下划线。"错峰"比"严格先后"更自然。
② 两条线的"擦除"基本同步 ------主体72%起擦,下划线74%起擦------差2%。
为什么不是"主体擦完再擦下划线"?
因为**"擦掉一个签名"是一个整体动作**------不是"先擦主字再擦下划线",而是整块内容一起被"抹掉"。
③ stroke-linecap: round让起笔收笔圆润
css
.stroke { stroke-linecap: round; stroke-linejoin: round; }
这是"手写质感"的最低成本------
stroke-linecap: round:线条的"两端"是圆头(不是平头)stroke-linejoin: round:线条的"转折处"是圆角(不是尖角)
少了这两行------签名的笔画会显得"生硬、像打印的"。
加上这两行------笔画立刻有"手写"的温度。
四、踩坑位(我全踩过,你直接避)
① 漏掉-webkit-background-clip: text
css
/* ❌ Safari 不认,光带扫不出来 */
background-clip: text;
/* ✅ 两行都写 */
-webkit-background-clip: text;
background-clip: text;
Safari只认带-webkit-前缀的版本------这是Safari的历史遗留。
同时还要配-webkit-text-fill-color: transparent:
css
/* ❌ 只写color: transparent,Safari里会"露底" */
color: transparent;
/* ✅ 两行都写 */
color: transparent;
-webkit-text-fill-color: transparent;
只写color: transparent在部分浏览器会显示出默认的黑色文字------光带效果看不到。
② background-size不放大
css
/* ❌ 渐变默认铺满100%,光带"扫"不动 */
background: linear-gradient(110deg, ...);
animation: sheen 4.2s linear infinite;
@keyframes sheen { from { background-position: 250% 0; } to { background-position: -250% 0; } }
/* ✅ 先放大到250%,position才有可动空间 */
background-size: 250% 100%;
background-position是"在背景图内部移动" ------如果背景图正好铺满元素 ,position怎么动,画面上都不会变化。
先放大background-size ------让背景图大于元素 ------光带才有"藏起来的空间" ------position才有"可动区间"。
这条规律通用 ------所有"扫光 / 滑动 / 移动"的background-position动画,都要先把background-size放大。
③ dasharray给小了
css
/* ❌ 路径真实长度超过dasharray,写完一半就断 */
.stroke { stroke-dasharray: 400; } /* 实际路径长度900 */
/* ✅ 给个足够大的数字,宁可浪费不可截断 */
.stroke { stroke-dasharray: 900; } /* 或1200,都行 */
stroke-dasharray必须 ≥ 路径的实际长度。
给小了 ------整条线被切成"实线 + 空白 + 实线 + 空白"------动画只能画出一部分。
如何量路径的真实长度?
浏览器控制台一行:
js
document.querySelector('.s1').getTotalLength();
返回一个数字 ------比如874.23------取整往上加一点(900) ------安全值。
拿不准就给天文数字 ------900 / 1200 / 2000------宁可浪费不可截断。
④ SVG path忘写fill: none
css
/* ❌ 曲线内部被默认黑色填充实心块,签名变墨渍 */
.stroke { /* 没有fill */ }
/* ✅ 明确fill: none */
.stroke { fill: none; }
SVG path默认fill 是黑色 ------如果不显式设为none,路径围出来的所有区域都会被填成黑色。
结果:你想要的"一条线",变成了"一块黑的墨渍"。
这是第一次写SVG路径最容易踩的坑 ------fill: none是必写项。
⑤ 擦除阶段没设计
css
/* ❌ 只有"写"没有"擦",循环时笔画闪断 */
@keyframes draw1 {
0% { stroke-dashoffset: 900; }
100% { stroke-dashoffset: 0; }
}
/* 循环重置时,笔画从0瞬间跳回900 ------ 闪一下 */
/* ✅ 让 offset 冲过 0 到负值,笔画反向消失 */
@keyframes draw1 {
0% { stroke-dashoffset: 900; }
30% { stroke-dashoffset: 0; } /* 写完 */
72% { stroke-dashoffset: 0; } /* 停笔 */
100% { stroke-dashoffset: -900; } /* 反向擦掉 */
}
只写"0 → 写完"的动画 ------循环重置时,笔画从"写完"瞬间跳回"隐藏" ------观感上就是闪一下。
加上"擦除段" ------offset从0推到-900------笔画从起笔端反向消失 ------循环天衣无缝。
这是"无缝循环"的又一条通用心法 ------让"结束状态"和"起始状态"视觉上一致。
- 流光:首尾都是基色
- 签名:首尾都是"隐藏"
五、变式挑战(10--30分钟,动手才有收获)
任务:签你自己的名字。
有两条路线:
路线A:手动画路径(推荐)
- 打开任意矢量软件(Figma / Illustrator / Inkscape)
- 用铅笔工具(Pencil)随手涂一笔------像签名一样
- 导出为SVG ------拿到
path的d属性 - 替换到
.s1的d="..."里
路线B:找现成的字(更工程化)
- 在网上搜任意手写风格字母的SVG path(很多免费字体站提供SVG)
- 复制
d属性替换进.s1 dasharray改成新路径的长度 ------浏览器控制台document.querySelector('.s1').getTotalLength()一行就能量出来- 调整动画时长------路径长的,动画时长要长一点,否则"笔"走太快
进阶方向(有余力再玩):
① 让签名的笔画有"粗细变化"
css
.s1 { stroke-width: 3.5; }
.s1:hover { stroke-width: 5; } /* hover时变粗 */
真实签名里,笔画的粗细是变化的 ------CSS做不到"沿路径变化线宽",但可以用两层路径叠出效果 (外层细、内层粗,用stroke-dashoffset错开)------高级玩法。
② 加"落笔的墨点"
css
.sig::before {
content: ""; position: absolute; ...; /* 一个圆点 */
animation: drop 6.5s ease-in-out infinite;
}
@keyframes drop {
0% { opacity: 0; }
5% { opacity: 1; } /* 起笔时墨点出现 */
10% { opacity: 0; } /* 墨水渗开,消失 */
}
"落笔的墨点"是手写感的加分项------签名开始的瞬间,一个小墨点出现又消失。
③ 换成中文名签名
中文名的难度在于笔画多 ------但stroke-dashoffset的原理完全一样。关键是把"多笔画"拆成多条path,各自按笔顺错峰动画。
这是最有意思的挑战 ------你可以在评论区贴出你的中文名签名,我会挑几个做特写拆解。
④ 让流光文字"循环更慢一点"
现在4.2s一次扫过 ------可以试试8s或10s ------"优雅"和"频率"成反比。
⑤ 让流光和签名"错峰"
现在两个动画独立跑(流光4.2s、签名6.5s)------可以让它们"同步" ------比如签名写完的瞬间,流光正好扫过 ------"笔落墨干,纸面反光"。
六、小结
今天这一篇里藏着 5个通用技能点:
| 技能 | 适用场景 |
|---|---|
background-clip: text + 透明文字 |
所有"渐变文字 / 金属字 / 流光字" |
background-size放大 + position滑动 |
所有"扫光 / 高光扫过"效果 |
stroke-dashoffset做"描边进度" |
所有"沿线书写 / 进度条 / 对勾动画" |
stroke-linecap: round手写质感 |
所有"手绘 / 手写 / 曲线"效果 |
| 首尾状态一致做无缝循环 | 所有循环动画的"重置隐蔽化" |
最值得记住的三条:
① 文字流光和手写签名,本质是同一件事。
"让一段渐变 / 线条,按进度展示。"
- 流光 :渐变 +
background-position动 - 签名 :SVG线 +
stroke-dashoffset动
**一个"填充"的进度,一个"描边"的进度。**理解了这个统一视角,你就能把两个效果都拆到最简单。
② background-size不放大,background-position就动不了。
这是"扫光"效果的唯一前提------
- 背景图 = 元素大小 :
position怎么动,画面不变 - 背景图 > 元素大小 :
position动起来,光带才"扫过"
"先放大,再滑动"------所有扫光效果的通用公式。
③ 无缝循环的关键,是"结束状态 = 起始状态"。
- 流光:首尾都是基色(光带藏在外面)
- 签名 :首尾都是隐藏(
offset冲过0到负值)
"让重置发生在看不见的时候"。
如果这一篇对你有帮助,三连支持一下这个专栏 🙏
点赞 = 更新加速,收藏 = 随时复制代码,评论 = 你想看什么效果,我排进日程。
本专栏所有demo,复制即用,随便商用。
目标更新:不断更。