1. 打字机效果到底是个什么东西第一次在某个产品官网上看到那种文字一个一个蹦出来的效果时我盯着看了好几秒。那种感觉就像有人在屏幕另一端实时敲字给你看比整段文字直接怼脸上要有意思得多。后来我在不少个人主页、产品落地页、甚至一些终端模拟器风格的网站上反复见到这种效果才意识到这已经是一个相当经典的交互细节了。打字机效果英文里常叫 typewriter effect 或者 typing animation核心表现就是一段文本像被逐字敲出来一样依次出现通常还会配一个闪烁的光标模拟输入焦点。它解决的问题很直接让静态的文字有了时间维度上的节奏感能引导用户的视线跟着文字走而不是一眼扫完就移开。适合用在首屏标语、终端风格展示、代码演示、对话式界面这些场景里。很多人第一反应是这得用 JavaScript 写吧监听个定时器每隔几十毫秒往 DOM 里塞一个字符。能用 JS 当然能做但如果你只是想要一个纯展示性的打字效果不涉及复杂的状态管理CSS 其实完全扛得住而且性能更好、代码更少、还不占用主线程。这篇就围绕纯 CSS 实现打字机效果这件事把思路、参数计算、实操步骤和踩过的坑一次讲清楚不管你是刚接触 CSS 动画的新手还是想找个轻量方案的老手都能直接抄作业。2. 整体思路与方案选型拆解2.1 为什么优先考虑纯 CSS 方案先说清楚一个前提纯 CSS 的打字机效果有它的适用边界。它最适合的是文本内容固定、不需要动态变更的场景。因为 CSS 没法真正去“读取”一段文字然后逐字输出它的本质是用视觉手段去“遮挡”和“揭示”。我选择纯 CSS 而不是 JS 的理由有这么几条。第一是性能CSS 动画跑在合成线程上不阻塞主线程页面其他交互不会因为打字动画而卡顿。第二是代码量一个成熟的 CSS 打字方案核心代码不超过二十行而 JS 版本要考虑定时器管理、暂停恢复、多段文本切换代码量翻好几倍。第三是可维护性样式和结构分离得干净改个速度就是改一个变量的事。当然如果你的需求是“用户点击按钮后重新打字”“根据接口返回内容动态打字”那还是老老实实上 JSCSS 硬撑只会把代码写成一团乱麻。判断标准很简单内容是不是写死的。写死的CSS 上动态的JS 上。2.2 核心原理宽度动画加光标闪烁纯 CSS 打字机效果的底层原理其实特别朴素拆开看就两个动作在同时进行。第一个动作是控制文本容器的宽度。把一段文字放在一个容器里容器设置overflow: hidden并且white-space: nowrap这样文字就不会换行。然后让容器的宽度从 0 慢慢变到文字的完整宽度视觉上就像是文字被一个字一个字“推”出来一样。这里的关键在于宽度动画的节奏——如果匀速展开看起来就像拉幕布如果配合steps()函数做阶梯式变化才会有一格一格蹦字的感觉。第二个动作是光标的闪烁。用一个伪元素或者一个独立的元素做光标给它一个blink动画让透明度在 0 和 1 之间来回切换。这个动画的周期通常设在 0.5 秒到 1 秒之间太快了晃眼太慢了显得呆。这两个动作叠加在一起就构成了我们看到的打字机效果。理解了这个本质后面所有的参数调整都是在围绕这两个动作做微调。2.3 关键函数 steps() 的作用机制steps()是 CSS 动画里一个容易被忽略但在这里至关重要的函数。普通的linear缓动是连续变化的宽度会平滑增长而steps(n)会把整个动画过程切成 n 个等份每一份之间是瞬间跳变的。打个比方linear像是电梯匀速上升你能看到楼层数字连续滚动steps()像是楼梯你是一级一级跳上去的。打字机效果要的就是这种“跳”的感觉因为真实的打字就是离散的字符输入不是连续的。steps()的参数里还有一个step-end和step-start的区别这个后面实操部分会详细讲因为它直接决定了光标和文字的对齐关系是新手最容易翻车的地方。3. 核心细节解析与实操要点3.1 容器宽度到底该设多少这是整个方案里最容易出问题的地方。宽度动画的终点值必须精确等于文字的完整渲染宽度多一个像素会露出空白少一个像素会截断最后一个字。问题在于文字的渲染宽度受字体、字号、字重、字间距、浏览器渲染引擎等多重因素影响你没法用一个固定的像素值去适配所有情况。我试过直接写死width: 300px结果换个字体就崩了。比较稳妥的做法是用ch单位。ch是 CSS 里一个相对单位1ch 大约等于当前字体下数字 “0” 的宽度。对于等宽字体来说每个字符宽度一致用ch单位就非常精确。比如一段 20 个字符的文本宽度设成20ch基本就对了。但如果是非等宽字体ch单位就不准了因为字母 i 和字母 W 的宽度差很多。这种情况下我一般用em单位估算或者干脆用max-content配合动画。不过max-content在动画里不能直接做插值需要绕一下后面实操部分会给具体写法。提示如果你的文本是中文中文字符宽度基本一致用ch单位反而比英文更准因为中文没有 i 和 W 那种极端宽度差。3.2 光标用伪元素还是独立元素光标有两种实现方式各有取舍。用::after伪元素的好处是 HTML 结构干净光标天然跟在文字后面不需要额外标签。缺点是伪元素的样式控制相对受限而且当容器宽度在动画时伪元素的位置会跟着容器边缘走需要处理好定位关系。用独立span元素的好处是控制灵活可以单独给光标加动画、改颜色、调大小甚至做多个光标。缺点是 HTML 里多了一个标签语义上稍微冗余。我个人的习惯是单行文本用伪元素多行或需要复杂光标效果用独立元素。大部分场景下伪元素就够了代码更简洁。3.3 动画时长的计算逻辑打字速度直接决定观感。太快了像闪一下太慢了用户等得着急。我的经验值是每个字符 0.1 秒到 0.15 秒比较舒服接近真人快速打字的节奏。假设文本有 20 个字符每字 0.1 秒那总时长就是 2 秒。这个时长要同时应用到宽度动画和 steps 的步数上。steps 的步数应该等于字符数这样每一格刚好对应一个字符的宽度。这里有个细节如果文本里包含空格空格也算一个字符宽度steps 的步数要把空格算进去。我见过有人漏算空格导致最后几个字挤在一起排查半天才发现是步数少了一个。3.4 光标闪烁与打字动画的同步光标闪烁动画和打字动画是两个独立的动画它们同时运行但周期不同。打字动画是一次性的跑完就停光标闪烁是无限循环的一直闪。这里要注意的是动画填充模式。打字动画结束后宽度必须保持在最终值不能弹回去所以要用animation-fill-mode: forwards。光标动画则用infinite无限循环。还有一个容易忽略的点打字动画结束后光标最好继续闪烁模拟“等待输入”的状态。如果打字一结束光标就消失会显得很突兀。所以两个动画要分开写不要合并到一个 animation 声明里。4. 完整实操过程与代码实现4.1 HTML 结构搭建结构部分极简一个容器包一段文字就行。div classtypewriter span classtypewriter-text你好欢迎来到我的主页/span /div如果你打算用伪元素做光标那.typewriter-text这个 span 甚至可以省掉直接把文字放在.typewriter里。但我建议保留这个 span因为后面控制宽度动画作用在文字元素上比作用在容器上更清晰容器只负责overflow: hidden的裁剪工作。4.2 基础样式与裁剪设置先给容器和文字元素打底。.typewriter { display: inline-block; overflow: hidden; white-space: nowrap; border-right: 2px solid #333; font-family: monospace; font-size: 24px; }这里几个属性各有分工。display: inline-block让容器宽度能跟随内容收缩同时又能接受宽度动画。overflow: hidden是裁剪的关键超出容器宽度的文字会被藏起来。white-space: nowrap强制不换行否则文字会在容器变宽的过程中折行效果全毁。border-right这里我先用了一个简单的右边框当光标这是最省事的做法后面会换成更灵活的伪元素方案。font-family: monospace用等宽字体这样ch单位才准确。4.3 宽度动画与 steps 配置核心动画来了。.typewriter { width: 0; animation: typing 2s steps(12, step-end) forwards; } keyframes typing { from { width: 0; } to { width: 12ch; } }这段代码里steps(12, step-end)的 12 是字符数step-end表示在每个步进周期的末尾发生跳变。forwards保证动画结束后宽度停在12ch。关于step-end和step-start的选择我踩过坑。用step-start的话动画一开始宽度就直接跳到第一格的终点第一个字符会瞬间出现没有“打出来”的过程。用step-end则是从 0 开始到第一格末尾才跳到第一格宽度第一个字符是“打”出来的。所以打字机效果必须用step-end。4.4 光标闪烁动画实现把边框光标换成伪元素控制更精细。.typewriter { border-right: none; position: relative; } .typewriter::after { content: ; position: absolute; right: 0; top: 0; width: 2px; height: 100%; background-color: #333; animation: blink 0.8s step-end infinite; } keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }这里光标用绝对定位贴在容器右边缘高度撑满。blink动画用step-end让透明度瞬间切换而不是渐变这样才有光标“啪”一下亮灭的感觉。如果用默认的ease光标会像呼吸灯一样渐隐渐现那就不像光标了。注意光标动画的step-end和打字动画的step-end是两回事前者控制透明度跳变后者控制宽度跳变不要混淆。4.5 多行文本的处理方案单行文本用上面的方案就够了但如果你有一段多行文本想逐行打字纯 CSS 就有点吃力了。因为宽度动画只能控制水平方向多行需要垂直方向的揭示。一种变通做法是给每一行单独包一个容器每个容器设置不同的animation-delay让它们依次开始打字。这样虽然代码多了点但效果是可控的。.line-1 { animation-delay: 0s; } .line-2 { animation-delay: 2s; } .line-3 { animation-delay: 4s; }延迟时间等于上一行的打字时长这样第二行会在第一行打完后紧接着开始。如果想让它们有重叠就把延迟调小一点。4.6 响应式适配的注意事项在移动端或者窄屏下固定的ch宽度可能会超出屏幕。这时候要么缩小字号要么让文字换行。但换行和white-space: nowrap是冲突的所以响应式场景下我一般用媒体查询单独处理。media (max-width: 600px) { .typewriter { font-size: 16px; white-space: normal; animation: none; width: auto; } }窄屏下直接关掉动画让文字正常显示。这不是偷懒而是因为小屏幕上打字动画的体验并不好用户更希望快速看到完整内容。5. 常见问题与排查技巧实录5.1 文字抖动问题打字过程中文字位置轻微抖动这是最常见的问题之一。原因通常是宽度动画和字体渲染的舍入误差。浏览器在计算ch单位时会有小数而宽度动画的插值也会产生小数两者对不齐就会抖。解决办法是给文字元素加will-change: width或者transform: translateZ(0)触发 GPU 加速让渲染更稳定。另一个办法是把ch单位换成精确的像素值但这样就牺牲了字体无关性需要权衡。5.2 光标位置偏移光标没有紧贴文字末尾而是飘在中间或者超出容器这通常是定位基准的问题。如果光标用绝对定位right: 0是相对于容器的右边缘而容器宽度在动画中所以光标会跟着容器边缘走这是对的。但如果容器有 padding光标就会偏。排查思路先检查容器有没有意外的 padding 或 margin再检查光标的right值是不是 0。如果用的是伪元素确认position: relative加在了正确的父元素上。5.3 动画结束后宽度回弹动画跑完宽度弹回 0这是因为忘了加animation-fill-mode: forwards。这个属性告诉浏览器动画结束后保持最后一帧的状态。很多人写动画时只关注keyframes忽略了 fill-mode结果就是动画白做。5.4 不同字体下宽度不准等宽字体下ch单位很准但换成非等宽字体就会偏。这时候有两个选择一是坚持用等宽字体反正打字机效果配等宽字体本来就更有终端感二是用 JS 测量实际宽度后设置 CSS 变量。如果坚持纯 CSS我建议就用等宽字体省心。5.5 常见问题速查表问题现象可能原因解决方向文字抖动宽度插值舍入误差加 will-change 或 translateZ光标偏移容器有 padding 或定位基准错检查 padding 和 position 父级宽度回弹缺少 fill-mode加 animation-fill-mode: forwards宽度不准非等宽字体换等宽字体或用 JS 测量第一个字瞬间出现用了 step-start改成 step-end光标渐变而非闪烁光标动画没用 step-end给 blink 动画加 step-end5.6 几个我踩过的坑第一个坑是忘了算空格。有次文本里有个空格steps 步数按可见字符算的结果最后一个字被截了一半。后来养成习惯写 steps 之前先数一遍包括空格在内的所有字符。第二个坑是动画时长和 steps 步数不匹配。步数多了每个字符停留时间短看起来像快进步数少了几个字符一起蹦出来。步数必须严格等于字符数这个没有商量余地。第三个坑是在 flex 容器里用这套方案。flex 子项的宽度计算规则和普通块级元素不同宽度动画会失效。解决办法是给打字元素外面再包一层或者给打字元素设flex-shrink: 0。6. 进阶玩法与性能优化建议6.1 用 CSS 变量统一管理参数当页面里有多个打字机效果时把速度、字符数、颜色这些参数抽成 CSS 变量会方便很多。.typewriter { --char-count: 12; --type-speed: 0.15s; --cursor-color: #333; animation: typing calc(var(--char-count) * var(--type-speed)) steps(var(--char-count), step-end) forwards; }这样改速度只需要动一个变量不用去翻 keyframes。而且calc()配合变量做时长计算比手算靠谱。6.2 减少重排的性能技巧宽度动画会触发重排虽然现代浏览器对这类动画有优化但字符多的时候还是能感觉到。一个优化思路是用transform: scaleX()代替宽度动画因为 transform 只触发合成不触发重排。但 scaleX 会把文字也横向拉伸所以需要配合transform-origin: left和反向缩放文字实现起来复杂不少。除非你的文本特别长比如超过 50 个字符否则宽度动画的性能足够用没必要上这个方案。6.3 尊重用户的减弱动画偏好有些用户系统里开启了“减弱动态效果”这时候打字动画可能会让他们不适。用媒体查询检测这个偏好直接关掉动画。media (prefers-reduced-motion: reduce) { .typewriter { animation: none; width: auto; } .typewriter::after { animation: none; } }这是一个无障碍方面的细节加上它不费什么事但体现了对用户的尊重。6.4 和其他 CSS 效果的组合打字机效果可以和渐变文字、发光效果、背景色块组合。比如给文字加个background-clip: text做渐变填充打字过程中颜色跟着宽度一起揭示视觉层次会更丰富。但要注意组合效果越多浏览器渲染压力越大尤其是在低端设备上。我的建议是最多叠加两个效果再多就过了反而显得花哨。7. 写在最后的一点个人体会这套纯 CSS 打字机方案我从几年前开始用中间改过好几版从最早的边框光标到现在的伪元素方案从写死像素宽度到用 ch 单位每一步调整都是被实际问题逼出来的。它不是什么高深技术但细节特别多一个参数没对上效果就出不来。我现在做项目时的习惯是先把 HTML 和基础样式搭好确认文字能正常显示再一步步加动画。先加宽度动画调好 steps 和时长确认打字节奏对了最后再加光标。分步调试比一次性写完再排查要快得多因为出问题时你能立刻定位到是哪一层出的错。另外提醒一句打字机效果虽好但别滥用。一个页面里出现两三个就差不多了到处都是打字动画会让用户觉得烦躁。它适合用在需要强调的、一次性的展示场景而不是所有文字都来一遍。这个度得自己把握。
阅读完成 · 觉得有帮助?