GODRICH
#cssアニメーション#ボタン#ローディング#トランジション#スクロール#ダッシュボード#実用ui#デザイントレンド

【コピペOK】css テキスト アニメーション 9選

css テキスト アニメーション とは、見出しや文章が画面に現れるときに一緒に動く演出のことです。見出しを一気に引き立てる一瞬のつくり方を9つ集めました。そのうち1つは、暗号が解けるように文字がそろいます。

自動再生 · タイルを押すとその項目へ · すべて1つのzip

並び順は人気順ではなく、文章が画面に現れるまでの流れに沿っています。まずは単語や行の単位でふわりと浮かび上がったり滑り込んだりする登場(01〜04)、次に機械式の表示板のようにめくれる演出と暗号が解けるように文字がそろう遊び心のある登場(05〜06)、最後は、すでにそこにある見出しそのものを強める演出、つまり字間を詰める、輪郭を塗る、影を重ねる(07〜09)です。文字ではなく板そのものが回って中身を変える側はカードフリップのアニメーションが担当し、カーソルに反応するカードの表面はCSSカードホバー効果に集まっています。

01単語ごとに浮かび上がる

単語のまとまりで動く

文章が単語ごとに区切られ、下から上へ1つずつ浮かび上がります。文字単位ではなく単語のまとまりで動くので、目で追いやすいテンポになります。

単語ごとに浮かび上がる
新しいタブで開く

01 · 単語ごとに浮かび上がる — 計測 累積面積 5.099% · 動いたフレーム 13/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

display:inline-blocktranslateYanimation-delay

違うのは delay だけ

単語ごとに違うのは animation-delay の 0・160ms・320ms だけで、このずれが効果のすべてです。登場区間は慣性 lerp 曲線で目標を少し通り過ぎてから定着し、単語ごとに blur の深さ(6→9→12px)も変えて重い単語ほど遅く焦点が合います。

.ws-word {
  display: inline-block;
  opacity: 0;
  transform: translateY(16px);
  filter: blur(var(--blur, 6px));
}
@keyframes ws-rise {
  0% { opacity: 0; transform: translateY(16px); filter: blur(var(--blur, 6px)); animation-timing-function: $ease-lerp-settle; }
  20%, 80% { opacity: 1; transform: none; filter: blur(0); }
  100% { opacity: 0; transform: translateY(16px); filter: blur(var(--blur, 6px)); }
}
.ws-line.is-demo .ws-word { animation: ws-rise 2s ease-out infinite; }
.ws-line.is-demo .ws-word:nth-child(2) { animation-delay: 160ms; --blur: 9px; }
.ws-line.is-demo .ws-word:nth-child(3) { animation-delay: 320ms; --blur: 12px; }

02斜めワイプマスク

斜めに切り開かれる

斜めの境界線が横切ると、その後ろから文字が現れます。カーテンのように持ち上がるのではなく、斜めに切り開かれる見え方です。

斜めワイプマスク
新しいタブで開く

02 · 斜めワイプマスク — 計測 累積面積 2.394% · 動いたフレーム 9/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

skewX(-18deg)transition0.7s

継ぎ目が見えない理由

マスクの色を背景とまったく同じにしてあるため、skewX をかけた板を translateX で送り出すだけで継ぎ目が見えません。マスクは慣性 lerp 曲線で、目標位置を少し通り過ぎてから定着します。

.dw-mask {
  position: absolute;
  inset: -20% -10%;
  background: $stage-yellow;
  transform: skewX(-18deg) translateX(0);
}
@keyframes dw-sweep {
  0%, 10% { transform: skewX(-18deg) translateX(0); }
  45%, 70% { transform: skewX(-18deg) translateX(140%); }
  100% { transform: skewX(-18deg) translateX(0); }
}
.dw-wrap.is-demo .dw-mask { animation: dw-sweep 2s $ease-lerp-settle infinite; }

03拡大ポップイン

行き過ぎてから戻る

文字が小さく縮んだ状態から本来のサイズを少し超えて広がり、跳ねるように元へ戻ります。

拡大ポップイン
新しいタブで開く

03 · 拡大ポップイン — 計測 累積面積 6.413% · 動いたフレーム 13/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

scalecubic-bezierovershoot

弾みを担う曲線

この行き過ぎはキーフレーム1つ分だけで、弾む感じは cubic-bezier(.2, 1.4, .4, 1) が担っています。登場区間は慣性 lerp 曲線で目標を少し通り過ぎてから定着するため、大きくなる速度に重みが乗ります。

.sp-text {
  opacity: 0;
  transform: scale(.4);
}
@keyframes sp-pop {
  0% { opacity: 0; transform: scale(.4); animation-timing-function: $ease-lerp-settle; }
  18% { opacity: 1; transform: scale(1.1); }
  30% { transform: scale(.96); }
  40%, 78% { transform: scale(1); opacity: 1; }
  100% { opacity: 0; transform: scale(.4); }
}
.sp-text.is-demo { animation: sp-pop 2s cubic-bezier(.2, 1.4, .4, 1) infinite; }

04行ごとに互い違いスライド

左右から互い違いに

2行の段落で、奇数行は左から、偶数行は右から滑り込んでジグザグに収まります。同じ 40px の translateX を符号だけ反転させ、160ms の差をつけただけの仕組みです。

行ごとに互い違いスライド
新しいタブで開く

04 · 行ごとに互い違いスライド — 計測 累積面積 7.392% · 動いたフレーム 12/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

translateXanimation-delay: 160msalternating delay

行が増えても計算不要

左右の振り分けは --l と --r のクラス名が決めるので、行が増えても方向を計算するスクリプトはいりません。各行は慣性 lerp 曲線で定着し、同じ区間で blur(7px)→0 に焦点が合います。

.ls-line--l { transform: translateX(-40px); }
.ls-line--r { transform: translateX(40px); }
@keyframes ls-in-r {
  0% { opacity: 0; transform: translateX(40px); filter: blur(7px); }
  25%, 80% { opacity: 1; transform: none; filter: blur(0); }
  100% { opacity: 0; transform: translateX(40px); filter: blur(7px); }
}
.ls-block.is-demo .ls-line--r {
  animation: ls-in-r 2s $ease-lerp-settle infinite;
  animation-delay: 160ms;
}

05スプリットフラップ表示

文字板がめくれて止まる

空港の案内板のように文字板が上から下へめくれ、正しい文字で止まります。機械らしさを生むのは変形ではなくタイミングで、steps(3, end) がふつうの rotateX を3段階に切り刻んでいます。

スプリットフラップ表示
新しいタブで開く

05 · スプリットフラップ表示 — 計測 累積面積 5.078% · 動いたフレーム 11/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

rotateXsteps()perspective

機械らしさはタイミング

ここをイージング曲線に替えると、ただの滑らかな3Dフリップになってしまいます。板が縁を向く地点で少しぼやけ、着地で焦点が合うモーションブラーの一層を重ねました。

.sf-char {
  transform-origin: center;
  transform: rotateX(90deg);
  opacity: 0;
  filter: blur(3px);
}
@keyframes sf-flip {
  0% { transform: rotateX(90deg); opacity: 0; filter: blur(3px); }
  15% { filter: blur(4px); }
  30% { transform: rotateX(0deg); opacity: 1; filter: blur(0); }
  90% { transform: rotateX(0deg); opacity: 1; filter: blur(0); }
  95% { filter: blur(4px); }
  100% { transform: rotateX(90deg); opacity: 0; filter: blur(3px); }
}
.sf-board.is-demo .sf-char { animation: sf-flip 2s steps(3, end) infinite; }
.sf-board.is-demo .sf-char:nth-child(2) { animation-delay: 120ms; }

06スクランブルデコード

css テキスト アニメーション 一文字ずつ固定していく

ランダムな文字が 60ms ごとに入れ替わり、左から1文字ずつ本来の文字に固定されていきます。

スクランブルデコード
新しいタブで開く

06 · スクランブルデコード — 計測 累積面積 2.665% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

setIntervalJS 12줄monospace

CSSでは書き換えられない

CSS では文字の内容そのものを書き換えられないため、これだけが @keyframes を一切持たず、JavaScriptで動く項目になっています。固定された文字数がそのまま進行率なので、別途イージングを計算する必要はありません。

var frame = 0;
setInterval(function () {
  frame++;
  var locked = Math.max(0, Math.floor((frame % 26) / 3) - 2);
  var out = "";
  for (var i = 0; i < target.length; i++) {
    out += i < locked ? target[i] : glyphs[Math.floor(Math.random() * glyphs.length)];
  }
  el.textContent = out;
}, 60);

07字間を詰めて登場

広がった状態から詰める

文字が大きく間隔を空けて透明なまま隠れた状態から、間隔を詰めながらはっきりした見出しへ集まります。

字間を詰めて登場
新しいタブで開く

07 · 字間を詰めて登場 — 計測 累積面積 3.327% · 動いたフレーム 11/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

custom propertytranslateXopacity

動かすのは transform だけ

letter-spacing を直接動かすと毎フレームレイアウトを計算し直すことになるので、文字ごとに --x という開始位置を持たせ、動かすのは transform だけにしてあります。収束区間は慣性 lerp 曲線で定着し、詰まるほど blur が晴れます。

.tk-ch {
  display: inline-block;
  opacity: 0;
  transform: translateX(var(--x, 0));
  filter: blur(6px);
}
@keyframes tk-converge {
  0% { opacity: 0; transform: translateX(var(--x, 0)); filter: blur(6px); }
  35%, 80% { opacity: 1; transform: translateX(0); filter: blur(0); }
  100% { opacity: 0; transform: translateX(var(--x, 0)); filter: blur(6px); }
}
.tk-word.is-demo .tk-ch { animation: tk-converge 2s $ease-lerp-settle infinite; }

08輪郭から塗りつぶしへ

輪郭から色が流し込まれる

文字がまず輪郭だけの状態で現れ、左から色が流し込まれて塗りつぶされた文字になります。

輪郭から塗りつぶしへ
新しいタブで開く

08 · 輪郭から塗りつぶしへ — 計測 累積面積 2.835% · 動いたフレーム 13/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

-webkit-text-strokeclip-pathoverflow:hidden

2層を重ねる構成

下に -webkit-text-stroke の輪郭レイヤーを敷き、その上に重ねた色付きの文字を clip-path で左から開いていく構成です。塗りつぶしが流れ込む間、blur(6px)→0 でインクが染み込むように焦点が合います。

.fl-text--outline {
  color: transparent;
  -webkit-text-stroke: 1px $color;
}
.fl-text--fill {
  position: absolute;
  inset: 0;
  color: $color;
  clip-path: inset(0 100% 0 0);
  filter: blur(6px);
}
@keyframes fl-wipe {
  0%, 10% { clip-path: inset(0 100% 0 0); filter: blur(6px); }
  55%, 70% { clip-path: inset(0 0 0 0); filter: blur(0); }
  90% { clip-path: inset(0 0 0 0); filter: blur(0); }
  100% { clip-path: inset(0 100% 0 0); filter: blur(6px); }
}
.fl-wrap.is-demo .fl-text--fill { animation: fl-wipe 2s ease-out infinite; }

09影を重ねて立体化

影が重なって厚みが出る

平らだった文字の背後に影の層が重なって厚みが生まれ、また平らに戻ります。

影を重ねて立体化
新しいタブで開く

09 · 影を重ねて立体化 — 計測 累積面積 4.672% · 動いたフレーム 11/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

text-shadowmultiple layerstransition

変わるのはプロパティ1つ

1pxから4pxまでずらした4枚とぼかした1枚を text-shadow 1つの値にまとめてあるので、変わるのはプロパティ1つだけです。層は慣性 lerp 曲線で目標の厚みを少し通り過ぎてから定着し、重なるほど blur が晴れます。

.sh-text { text-shadow: none; filter: blur(5px); }
@keyframes sh-extrude {
  0%, 15% { text-shadow: none; filter: blur(5px); }
  55%, 85% {
    text-shadow:
      1px 1px 0 $subject-blue,
      2px 2px 0 $subject-blue,
      3px 3px 0 $subject-blue,
      4px 4px 0 $subject-blue,
      5px 5px 8px rgba(0, 0, 0, .35);
    filter: blur(0);
  }
  100% { text-shadow: none; filter: blur(5px); }
}
.sh-text.is-demo { animation: sh-extrude 2s $ease-lerp-settle infinite; }

css テキスト アニメーション はどこで壊れるか

色付きが輪郭を覆った

最初につまずいたのは08番でした。輪郭の文字の上に色付きの文字をただ重ねただけだったので、絶対配置で常に前面にある色付きレイヤーが輪郭を完全に覆ってしまい、輪郭から塗られていく過程が画面に一度も出てきませんでした。外から見るとループは正常に回っているように見えましたが、収録したフレームを1枚ずつ開くと、3枚目には色付きの文字が1つだけ残り、あとはほぼ空の画面でした。

逆の手が効いた

transform だけで済ませたくて、背景と同じ色の覆いを滑らせて隠す作りにしていたのですが、その覆いこそが見せたい段階を消していたわけです。効いたのは逆の手で、色付きレイヤー自身に clip-path をかけて左から広げると、輪郭の段階がようやく見えるようになりました。直したあとのソースは9つ分そろえてあり、解凍パスワード jagvrc78 で開く zip にはプレーンなCSS版とReact版の両方が入っています。

文字ごとに影を積んで限界を超えるこの地点は、質感を何層も重ねるスキューモーフィズムのLPキットで面の単位として同じ限界に出会います。

アクセシビリティ — 9つが reduced-motion で残すもの

止まって最終状態で残る

prefers-reduced-motion: reduce を有効にしている人には、9つとも動きを止めて最終状態のまま表示されます。SCSSの @keyframes で動く8つは、その最終状態を !important で固定してあります。単語は上がりきり、斜めのマスクは文字の外へ抜けきり、字間は詰まり、輪郭は塗られ、影は重なった姿です。動きを切っていても空の画面ではなく結果が見えます。

06番だけ違う理由

06番だけはCSSアニメーションを持たないので、スクリプトの中で matchMedia を確認し、文字を混ぜずに完成した単語をそのまま表示します。

関連リンク

css テキスト アニメーション サンプル をもっと見たい場合は CSS 文字 アニメーション 9選 が参考になり、全体の一覧は CSSのみカテゴリ にまとめてあります。08番で使った書き方は MDN の clip-path の解説で確認できます。

FAQ — よくある質問

文字 アニメーション css にJavaScriptは必要ですか

ほとんどは不要です。9つのうち8つはSCSSの @keyframes だけで動き、06番のスクランブルデコードだけが文字の内容を入れ替える必要があるため、短いJavaScriptを使います。

見出しには01番と04番のどちらが向いていますか

1行から2行の短い見出しなら、01番のように単語ごとに区切るほうが自然に読めます。2行から3行にわたる宣言文やミッションステートメントなら、行単位で左右から入る04番のほうが、単語単位では出せないリズムを生みます。

zip の中の vanilla と React でコードは違いますか

見た目の動きは両方とも同じです。違うのはきっかけで、vanilla版は is-demo クラスで強制的に繰り返し再生させているのに対し、React版は IntersectionObserver で画面に入った瞬間に一度だけ再生します。登場の演出を暗号が解けるような形へ変えたいなら、デコードのテキスト演出がその変奏です。

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。