kinetic-typography

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Typography in Motion

动态文字排版

Animate text expressively: splitting copy into lines/words/characters, staggering reveals, masking, blurring, morphing weight, and running text along a path. Produces runnable code for the web (CSS, GSAP, Framer Motion) and video (Remotion), plus an After Effects expression approach.
实现富有表现力的文字动画:将文本拆分为行/单词/字符、错开展示、遮罩、模糊、字体粗细变形,以及文字沿路径排列。提供可直接运行的Web端代码(CSS、GSAP、Framer Motion)和视频端代码(Remotion),还有After Effects表达式实现方案。

When to use

使用场景

  • Headline / tagline entrances and animated title cards.
  • Lyric videos, caption/subtitle reveals, kinetic-typography pieces.
  • Variable-font animation (weight, width, slant, optical size).
  • Text-on-path, text stroke draw-on, and per-character morph effects.
  • 标题/标语入场动画及动画标题卡片。
  • 歌词视频、字幕/副标题展示、动态排版作品。
  • 可变字体动画(粗细、宽度、倾斜、光学尺寸)。
  • 文字沿路径排列、文字描边绘制、逐字符变形效果。

Core workflow

核心工作流程

1. Get static typography right first

1. 先搞定静态文字排版

Motion cannot rescue bad type. Before animating, lock line-height (1.1-1.2 for display headlines, 1.4-1.6 for body), tracking (tighten display by -1% to -3%, e.g.
letter-spacing: -0.02em
), alignment, and weight. Then split and animate.
动效无法拯救糟糕的文字排版。在制作动画前,先锁定行高(展示类标题为1.1-1.2,正文为1.4-1.6)、字距(展示类文字收紧-1%至-3%,例如
letter-spacing: -0.02em
)、对齐方式和字体粗细。之后再拆分并制作动画。

2. Choose split granularity

2. 选择拆分粒度

  • By line — calmest, most premium; best for multi-line paragraphs and elegant headlines.
  • By word — energetic, good for taglines and lyric hits.
  • By character — most kinetic/playful; risk of feeling busy on long copy. Reserve for short strings.
Tools:
SplitType
or GSAP
SplitText
on the web;
.split('')
/
.split(' ')
plus
interpolate
in Remotion;
motion
staggered children in Framer Motion.
Critical accessibility note: splitting into per-character spans destroys the text for screen readers and breaks copy-paste. Always set
aria-label
on the container with the full string and
aria-hidden="true"
on the split fragments.
  • 按行拆分 — 最沉稳、高端;适用于多行段落和优雅的标题。
  • 按单词拆分 — 富有活力,适合标语和歌词重点部分。
  • 按字符拆分 — 最具动感/趣味性;长文本使用可能显得杂乱。仅用于短文本字符串。
工具:Web端使用
SplitType
或GSAP
SplitText
;Remotion中使用
.split('')
/
.split(' ')
搭配
interpolate
;Framer Motion中使用
motion
组件的错开子元素功能。
重要无障碍提示:将文本拆分为单个字符的span标签会破坏屏幕阅读器的文本识别,并导致无法复制粘贴。务必在容器上设置
aria-label
属性,填入完整文本内容,并在拆分后的片段上设置
aria-hidden="true"

3. Apply a reveal technique

3. 应用展示技巧

The four workhorse reveals:
  • Mask / clip reveal (most robust) — wrap each line in
    overflow: hidden
    ; animate the child from
    translateY(100%)
    to
    0
    . The text wipes up from a hidden baseline. No blur artifacts, GPU-cheap, the industry-standard headline reveal.
  • Blur-in — animate
    filter: blur(12px) -> blur(0)
    with
    opacity 0 -> 1
    . Soft, premium, "focus-pull" feel. Costlier to render; keep blur radius modest and prefer short durations.
  • Clip-path wipe — animate
    clip-path: inset(0 100% 0 0)
    to
    inset(0 0 0 0)
    for a directional reveal without moving the glyphs.
  • Character stagger — split to chars, fade/translate each with a 20-40ms offset.
四种常用的展示方式:
  • 遮罩/裁剪展示(最稳定) — 每行文字包裹在
    overflow: hidden
    的容器中;将子元素从
    translateY(100%)
    动画到
    0
    。文字从隐藏的基线向上滑入。无模糊 artifacts,GPU负载低,是行业标准的标题展示动画。
  • 模糊淡入 — 动画
    filter: blur(12px) -> blur(0)
    搭配
    opacity 0 -> 1
    。柔和、高端,有“聚焦”的感觉。渲染成本较高;模糊半径适中,优先选择短时长。
  • 裁剪路径擦除 — 动画
    clip-path: inset(0 100% 0 0)
    inset(0 0 0 0)
    ,实现文字不移动的定向展示效果。
  • 字符错开 — 拆分为字符,每个字符以20-40ms的偏移量进行淡入/位移动画。

4. Apply principle-correct timing

4. 应用符合原则的时序

Inline essentials (so this skill stands alone):
  • Enter with ease-out:
    cubic-bezier(0.16, 1, 0.3, 1)
    (easeOutExpo) feels premium.
  • Per-fragment duration 400-600ms.
  • Stagger: lines 60-100ms, words 40-70ms, characters 20-40ms apart.
  • Cap total reveal near ~800ms; for long strings, reduce per-char offset or switch to word/line granularity.
核心要点(确保此技能可独立使用):
  • 入场动画使用ease-out曲线:
    cubic-bezier(0.16, 1, 0.3, 1)
    (easeOutExpo),质感高端。
  • 每个片段的时长为400-600ms。
  • 错开时间:行60-100ms,单词40-70ms,字符20-40ms。
  • 总展示时长控制在约800ms以内;对于长文本,减少字符偏移量或切换为按单词/行拆分的粒度。

5. Mask-reveal, copy-paste (vanilla CSS + JS line split)

5. 遮罩展示示例,可直接复制粘贴(原生CSS + JS按行拆分)

html
<h1 class="reveal" aria-label="Designed for motion">Designed for motion</h1>
css
.reveal .line { overflow: hidden; }
.reveal .line > span {
  display: inline-block;
  transform: translateY(110%);
  animation: rise 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
.reveal .line:nth-child(2) > span { animation-delay: 0.08s; }
.reveal .line:nth-child(3) > span { animation-delay: 0.16s; }
@keyframes rise { to { transform: translateY(0); } }
html
<h1 class="reveal" aria-label="Designed for motion">Designed for motion</h1>
css
.reveal .line { overflow: hidden; }
.reveal .line > span {
  display: inline-block;
  transform: translateY(110%);
  animation: rise 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
.reveal .line:nth-child(2) > span { animation-delay: 0.08s; }
.reveal .line:nth-child(3) > span { animation-delay: 0.16s; }
@keyframes rise { to { transform: translateY(0); } }

6. Variable-font weight animation

6. 可变字体粗细动画

css
@keyframes thicken {
  from { font-variation-settings: "wght" 200; }
  to   { font-variation-settings: "wght" 800; }
}
.headline {
  font-family: "Inter var", sans-serif;
  animation: thicken 0.8s cubic-bezier(0.65, 0, 0.35, 1) forwards;
}
Animate
font-variation-settings
(covers any axis:
wght
,
wdth
,
slnt
,
opsz
, plus custom axes), not
font-weight
, to hit non-standard values and to interpolate smoothly. Confirm the loaded font is actually a variable font and the axis range (e.g. wght 100-900) before animating, or the value clamps silently.
css
@keyframes thicken {
  from { font-variation-settings: "wght" 200; }
  to   { font-variation-settings: "wght" 800; }
}
.headline {
  font-family: "Inter var", sans-serif;
  animation: thicken 0.8s cubic-bezier(0.65, 0, 0.35, 1) forwards;
}
应动画
font-variation-settings
(涵盖所有轴:
wght
wdth
slnt
opsz
以及自定义轴),而非
font-weight
,这样可以实现非标准值的过渡和平滑插值。动画前请确认加载的字体确实是可变字体,且轴范围正确(例如wght 100-900),否则值会被静默截断。

Deliver & verify (standalone HTML)

交付与验证(独立HTML文件)

Packaged helper (
scripts/
):
scripts/seek-shot.sh anim.html 0 1.5 3
freezes the
?t=N
harness and screenshots each moment;
scripts/contact-sheet.sh sheet.png frame-*.png
tiles them for one-glance review. See
scripts/README.md
.
For a web text reveal / kinetic-title deliverable, ship one
.html
file that opens directly in a browser
— no build step. One file is the right tier for a headline, title card, or single reveal.
Output contract:
  • One
    .html
    file: markup with
    aria-label
    on the container, the reveal in inline
    <style>
    (or one inline GSAP
    <script>
    from CDN).
  • One animation driver — a CSS
    @keyframes
    reveal, or a single GSAP timeline; not both.
  • Include the freeze harness below so any moment can be screenshotted deterministically.
  • Split AFTER
    document.fonts.ready
    so line breaks are correct in the frozen frame.
Freeze harness — pin a frame for screenshots.
html
<script>
  const t = new URLSearchParams(location.search).get("t");
  if (t !== null) {
    // CSS @keyframes reveal:
    document.querySelectorAll(".reveal *").forEach(el => {
      el.style.animationDelay = (-parseFloat(t)) + "s";
      el.style.animationPlayState = "paused";
    });
    // GSAP timeline instead? → tl.pause(); tl.seek(parseFloat(t));
  }
  window.__ready = true;                                          // ready signal for headless wait
</script>
Verify loop — render → freeze → screenshot → check:
  1. Open the file frozen at start / mid / end:
    …/type.html?t=0
    ,
    ?t=<dur/2>
    ,
    ?t=<dur>
    .
  2. Screenshot each frozen frame.
  3. Check fidelity (matches the brief) and artifacts — clipped glyphs/descenders, masks that don't fully hide the baseline, FOUC or wrong line breaks before fonts load, blur fringing.
bash
npx playwright screenshot --wait-for-timeout=500 "file://$PWD/type.html?t=0.4" frame-mid.png
Before you finish:
  1. Opens standalone in a browser — no console errors, no missing CDN/font.
  2. One reveal driver;
    ?t=N
    freezes the exact frame correctly.
  3. Screenshotted at start / mid / end — no clipped glyphs, no FOUC, line breaks correct.
  4. prefers-reduced-motion
    honored (text shown in final state, no motion).
  5. aria-label
    on the container; split fragments
    aria-hidden
    — copy and screen-reader text intact.
打包助手脚本
scripts/
目录):
scripts/seek-shot.sh anim.html 0 1.5 3
会冻结
?t=N
控制的动画,并截取每个时刻的截图;
scripts/contact-sheet.sh sheet.png frame-*.png
将截图拼接成一张预览图,方便快速查看。详情见
scripts/README.md
对于Web端文本展示/动态标题的交付成果,需提供可直接在浏览器中打开的单个
.html
文件
— 无需构建步骤。单个文件是标题、标题卡片或单一展示效果的理想交付形式。
输出规范:
  • 单个
    .html
    文件:容器上带有
    aria-label
    的标记,内嵌
    <style>
    中的展示动画(或从CDN引入的单个GSAP
    <script>
    )。
  • 仅使用一种动画驱动方式 — CSS
    @keyframes
    展示动画,或单个GSAP时间轴;不要同时使用两种。
  • 包含下方的冻结工具,以便可以确定性地截取任意时刻的截图。
  • document.fonts.ready
    之后再拆分文本,确保冻结帧中的换行正确。
冻结工具 — 固定帧用于截图
html
<script>
  const t = new URLSearchParams(location.search).get("t");
  if (t !== null) {
    // CSS @keyframes 展示动画:
    document.querySelectorAll(".reveal *").forEach(el => {
      el.style.animationDelay = (-parseFloat(t)) + "s";
      el.style.animationPlayState = "paused";
    });
    // 如果是GSAP时间轴?→ tl.pause(); tl.seek(parseFloat(t));
  }
  window.__ready = true;                                          // 无头模式等待的就绪信号
</script>
验证流程 — 渲染 → 冻结 → 截图 → 检查:
  1. 打开文件并冻结在开始/中间/结束时刻:
    …/type.html?t=0
    ?t=<dur/2>
    ?t=<dur>
  2. 截取每个冻结帧的截图。
  3. 检查保真度(符合需求)和瑕疵 — 文字被裁剪/下行部分被遮挡、遮罩未完全隐藏基线、字体加载前出现FOUC(无样式内容闪烁)或错误换行、模糊边缘。
bash
npx playwright screenshot --wait-for-timeout=500 "file://$PWD/type.html?t=0.4" frame-mid.png
完成前检查:
  1. 可在浏览器中独立打开 — 无控制台错误,无缺失的CDN资源/字体。
  2. 仅使用一种展示驱动方式;
    ?t=N
    可正确冻结指定帧。
  3. 已截取开始/中间/结束时刻的截图 — 无文字裁剪、无FOUC、换行正确。
  4. 尊重
    prefers-reduced-motion
    设置(文本显示为最终状态,无动画)。
  5. 容器上带有
    aria-label
    ;拆分后的片段设置了
    aria-hidden
    — 可复制文本,且屏幕阅读器可识别完整文本。

Quick reference

快速参考

EffectCore CSS / approach
Mask up revealparent
overflow:hidden
; child
translateY(110%)->0
Blur in
filter: blur(12px)->0
+ opacity
Directional wipe
clip-path: inset(0 100% 0 0)->inset(0)
Char staggersplit chars, 20-40ms offset, fade+rise
Variable weightanimate
font-variation-settings:"wght" a->b
Text on pathSVG
<textPath href="#curve">
Stroke draw-onSVG text as path, animate
stroke-dashoffset
效果核心CSS/实现方式
遮罩向上展示父元素
overflow:hidden
;子元素
translateY(110%)->0
模糊淡入
filter: blur(12px)->0
+ 透明度
定向擦除
clip-path: inset(0 100% 0 0)->inset(0)
字符错开拆分为字符,20-40ms偏移量,淡入+上移
可变字体粗细动画
font-variation-settings:"wght" a->b
文字沿路径排列SVG
<textPath href="#curve">
描边绘制将SVG文字转为路径,动画
stroke-dashoffset

Text on a path (SVG)

文字沿路径排列(SVG)

html
<svg viewBox="0 0 600 200">
  <path id="curve" d="M20,150 Q300,20 580,150" fill="none" />
  <text font-size="42" fill="#fff">
    <textPath href="#curve" startOffset="0%">Follow the curve</textPath>
  </text>
</svg>
Animate the
startOffset
attribute (
0%
->
100%
) to scroll text along the path, or animate the path
d
to make text flow on a morphing curve.
html
<svg viewBox="0 0 600 200">
  <path id="curve" d="M20,150 Q300,20 580,150" fill="none" />
  <text font-size="42" fill="#fff">
    <textPath href="#curve" startOffset="0%">Follow the curve</textPath>
  </text>
</svg>
可通过动画
startOffset
属性(
0%
->
100%
)实现文字沿路径滚动,或动画路径的
d
属性,让文字在变形的曲线上流动。

Reference files

参考文件

  • references/reveal-recipes.md
    — copy-paste reveal cookbook: mask/blur/clip/char-stagger in CSS, GSAP SplitText timelines, Framer Motion staggered variants, Remotion per-character interpolation, variable-font keyframes, and a screen-reader-safe split helper.
  • references/reveal-recipes.md
    — 可直接复制的展示动画手册:CSS中的遮罩/模糊/裁剪/字符错开效果、GSAP SplitText时间轴、Framer Motion错开变体、Remotion逐字符插值、可变字体关键帧,以及屏幕阅读器友好的拆分工具。