steps()本质是跳帧而非平滑过渡,通过将动画切成n等份并每步硬切状态实现逐字符显隐;需配合width动画、overflow:hidden、white-space:nowrap及ch单位,且n必须等于字符数(含空格),end确保每步结束时切换。

steps() 的本质是「跳帧」,不是平滑过渡
很多人以为 steps() 是让动画“慢慢打出来”,其实它根本不会插值——它只在关键帧之间硬切。比如 steps(5, end) 表示把整个动画过程切成 5 等份,每一份都显示同一帧,到第 5 步末尾才跳到下一状态。打字机效果依赖的就是这种“逐字符显隐”的离散感。
常见错误是把 steps() 和 ease 混用,或者误设步数导致字符漏显、多显。真正起作用的是:元素宽度从 0 到 100%(或用 ch 单位匹配字符数),配合 overflow: hidden 和 white-space: nowrap,再靠 steps(n, end) 控制显隐节奏。
-
n应等于目标文本的字符数(含空格),否则最后一两个字可能不出现 -
end表示在每步**结束时**切换,更符合打字“写完才露出来”的直觉;用start会导致首字符初始就可见 - 别用
%做 width 动画——字体渲染和小数像素会让截断位置不准,优先用ch单位(1ch ≈ 一个字符宽度)
关键 CSS 写法:width + steps() + overflow
核心就是三行:控制宽度增长、用 steps() 分帧、裁掉溢出部分。不需要 JS,纯 CSS 就能跑起来。
span.typewriter {
display: inline-block;
overflow: hidden;
white-space: nowrap;
width: 0;
animation: typing 3s steps(12, end), blink 0.5s step-end infinite;
}
@keyframes typing {
from { width: 0 }
to { width: 12ch } /* 必须和 steps(12, end) 的 12 一致 */
}注意:12ch 要和文本实际字符数严格对应;如果文本是 “Hello world!”(共 12 字符),那就刚好。多了会卡住,少了会提前停。
立即学习“前端免费学习笔记(深入)”;
- 动画时长(如
3s)决定总耗时,不影响单字符速度——速度由步数和时长共同决定(每步约3s / 12 = 0.25s) - 额外加个
blink动画模拟光标闪烁,用step-end避免淡入淡出,保持硬切换感 - 如果文本含中文,
ch单位不一定准(中文字宽不统一),可改用em或固定像素(如width: 140px),但需手动测量
遇到中文/等宽字体不一致怎么办?
默认 ch 基于当前字体的“0”字宽,而中文、英文字体混排时,ch 值会漂移——比如思源黑体里“字”比“a”宽得多,steps(10, end) 配 width: 10ch 就可能切不准。
稳妥做法是换用等宽字体并锁定单位:
- 强制设置
font-family: 'Courier New', monospace,确保所有字符等宽 - 用
ch时,先用getComputedStyle(el).fontSize+ 字符数估算像素宽度,再转成px值写死 - 更灵活的方式:不用
width,改用mask-image或clip-path配合steps(),但兼容性稍差(Safari 对clip-path的动画支持弱)
为什么 animation-delay 不能直接控制“开始打字时间”?
看起来加 animation-delay: 1s 就能让动画晚 1 秒启动,但实际容易出错——因为 steps() 动画的“第一帧”在延迟结束后立刻执行,如果延迟时间不是动画周期的整数倍,会导致首步跳跃或卡顿。
- 正确做法:把延迟拆进
@keyframes里,例如0% { width: 0 }→33% { width: 0 }→100% { width: 12ch },再配animation-timing-function: steps(12, end) - 或者用
animation-play-state: paused+ JS 控制播放时机,避免 CSS 层面的时间耦合 - 最简方案:直接给容器加
opacity: 0,延迟后用 JS 改为opacity: 1触发动画——虽然绕了点,但最可控
真正难的不是写对第一行,而是文本长度动态变化、字体加载未完成、或用户缩放页面时的响应——这些场景下 ch 和 steps() 的配合会失效,得 fallback 到 JS 测量。


















