纯 CSS 能实现逐字动画,但仅限单行、定长文本;要支持换行、变速、暂停或中文标点节奏,必须用 JavaScript。

纯 CSS 能实现逐字动画,但仅限单行、定长文本;要支持换行、变速、暂停或中文标点节奏,必须用 JavaScript。
用 steps() 实现纯 CSS 逐字动画(适合标题类静态文案)
CSS 的 steps() 是唯一不依赖 JS 就能“卡帧”显示每个字符的方法,但它有硬性前提:
- 容器必须设
white-space: nowrap,否则换行会打乱 step 计数 - 动画控制的是
width,所以得给容器设固定宽度(推荐用20ch,不是100%) - 总字符数必须已知,
steps(32, end)中的32得手动写死——改文案就得同步改这个数字 - 中文、emoji、全角标点实际占位 ≠ 英文字符,
ch单位在某些字体下会偏移,建议预览时微调
示例关键片段:
@keyframes typing {
from { width: 0 }
to { width: 32ch } /* 这里 32 必须等于文字字符总数 */
}
.typing-text {
overflow: hidden;
white-space: nowrap;
border-right: 2px solid;
animation: typing 3.2s steps(32, end), blink-caret 0.75s step-end infinite;
}用 setTimeout + textContent 实现 JS 逐字(通用、可控、兼容好)
这是真正灵活的方案:不依赖字符数预设,天然支持换行、中英文混排、动态内容。核心是累加 textContent,而非拼接 innerHTML(后者会清空事件监听器、触发重排)。
立即学习“前端免费学习笔记(深入)”;
- 别用
innerHTML += char,它会反复解析 HTML,有 XSS 风险且性能差 - 用
el.textContent += char最安全,也最快 - 延迟时间别写死,比如
100ms—— 中文阅读节奏比英文慢,建议按字符类型区分:char.match(/[\u4e00-\u9fa5,。!?;:]/) ? 180 : 120 - 如果文本含
\n,split('\n')后对每行单独跑 type 函数,行间加setTimeout(..., 800)模拟思考停顿
最小可用代码:
function typeText(el, text, i = 0, speed = 150) {
if (i < text.length) {
el.textContent = text.slice(0, i + 1);
setTimeout(() => typeText(el, text, i + 1, speed), speed);
}
}
typeText(document.getElementById('text'), '你好,世界!');逐行显示时为什么不能只靠 CSS?
CSS 动画本质是属性变化,width 或 opacity 都无法感知换行符 \n。即使你把每行包成 <p></p>,CSS 也没法让第 2 行等第 1 行播完再启动——没有“队列”概念。
- JS 方案里,
split('\n')后用递归或for循环控制行序,每一行内部再逐字,逻辑清晰 - 常见翻车点:用
match(/.+/g)拆行,遇到空行或末尾换行会出null;坚持用split('\n')最稳 - 清空容器时,别写
el.innerHTML = ''—— 如果你给里面某个span绑了点击事件,它就丢了;用while (el.firstChild) el.removeChild(el.firstChild)
光标(caret)闪烁该用 border-right 还是 outline?
用 border-right。原因很实在:
-
outline不随容器width收缩,动画过程中光标会“悬空”在文字右侧外边 -
border-right是盒模型一部分,width: 0 → 20ch时它自然跟着缩放 - 闪烁动画别用
opacity切换,直接切border-color:透明态用border-color: transparent,亮起时用border-color: #000,更准更轻量 - 注意:如果用了
font-variant-east-asian: tabular-nums或等宽字体,ch单位才真正可靠;否则中文字符宽度浮动,光标位置可能轻微抖动
真实项目里,最麻烦的永远不是第一行怎么动,而是用户中途刷新页面、焦点切换、或屏幕阅读器介入时,动画状态要不要保持、光标要不要重置——这些边界情况 JS 才能兜住,CSS 做不到。



















