【コピペOK】css テキスト アニメーション 9選
css テキスト アニメーション とは、見出しや文章が画面に現れるときに一緒に動く演出のことです。見出しを一気に引き立てる一瞬のつくり方を9つ集めました。そのうち1つは、暗号が解けるように文字がそろいます。
自動再生 · タイルを押すとその項目へ · すべて1つのzip
- 01 単語ごとに浮かび上がる
- 02 斜めワイプマスク
- 03 拡大ポップイン
- 04 行ごとに互い違いスライド
- 05 スプリットフラップ表示
- 06 スクランブルデコード
- 07 字間を詰めて登場
- 08 輪郭から塗りつぶしへ
- 09 影を重ねて立体化
並び順は人気順ではなく、文章が画面に現れるまでの流れに沿っています。まずは単語や行の単位でふわりと浮かび上がったり滑り込んだりする登場(01〜04)、次に機械式の表示板のようにめくれる演出と暗号が解けるように文字がそろう遊び心のある登場(05〜06)、最後は、すでにそこにある見出しそのものを強める演出、つまり字間を詰める、輪郭を塗る、影を重ねる(07〜09)です。
01単語ごとに浮かび上がる
文章が単語ごとに区切られ、下から上へ1つずつ浮かび上がります。文字単位ではなく単語のまとまりで動くので、目で追いやすいテンポになります。単語ごとに違うのは animation-delay の 0・160ms・320ms だけで、このずれが効果のすべてです。
.ws-word {
display: inline-block;
opacity: 0;
transform: translateY(16px);
}
@keyframes ws-rise {
0% { opacity: 0; transform: translateY(16px); }
20%, 80% { opacity: 1; transform: none; }
100% { opacity: 0; transform: translateY(16px); }
}
.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; }
02斜めワイプマスク
斜めの境界線が横切ると、その後ろから文字が現れます。カーテンのように持ち上がるのではなく、斜めに切り開かれる見え方です。マスクの色を背景とまったく同じにしてあるため、skewX をかけた板を translateX で送り出すだけで継ぎ目が見えません。
.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-out infinite; }
03拡大ポップイン
文字が小さく縮んだ状態から本来のサイズを少し超えて広がり、跳ねるように元へ戻ります。この行き過ぎはキーフレーム1つ分だけで、弾む感じは cubic-bezier(.2, 1.4, .4, 1) が担っています。
.sp-text {
opacity: 0;
transform: scale(.4);
}
@keyframes sp-pop {
0% { opacity: 0; transform: scale(.4); }
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 の差をつけただけの仕組みです。左右の振り分けは --l と --r のクラス名が決めるので、行が増えても方向を計算するスクリプトはいりません。
.ls-line--l { transform: translateX(-40px); }
.ls-line--r { transform: translateX(40px); }
@keyframes ls-in-r {
0% { opacity: 0; transform: translateX(40px); }
25%, 80% { opacity: 1; transform: none; }
100% { opacity: 0; transform: translateX(40px); }
}
.ls-block.is-demo .ls-line--r {
animation: ls-in-r 2s cubic-bezier(.2, .8, .2, 1) infinite;
animation-delay: 160ms;
}
05スプリットフラップ表示
空港の案内板のように文字板が上から下へめくれ、正しい文字で止まります。機械らしさを生むのは変形ではなくタイミングで、steps(3, end) がふつうの rotateX を3段階に切り刻んでいます。ここをイージング曲線に替えると、ただの滑らかな3Dフリップになってしまいます。
.sf-char {
transform-origin: center;
transform: rotateX(90deg);
opacity: 0;
}
@keyframes sf-flip {
0% { transform: rotateX(90deg); opacity: 0; }
30% { transform: rotateX(0deg); opacity: 1; }
90% { transform: rotateX(0deg); opacity: 1; }
100% { transform: rotateX(90deg); opacity: 0; }
}
.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スクランブルデコード
ランダムな文字が 60ms ごとに入れ替わり、左から1文字ずつ本来の文字に固定されていきます。CSS では文字の内容そのものを書き換えられないため、これだけが @keyframes を一切持たず、18行の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字間を詰めて登場
文字が大きく間隔を空けて透明なまま隠れた状態から、間隔を詰めながらはっきりした見出しへ集まります。letter-spacing を直接動かすと毎フレームレイアウトを計算し直すことになるので、文字ごとに --x という開始位置を持たせ、動かすのは transform だけにしてあります。
.tk-ch {
display: inline-block;
opacity: 0;
transform: translateX(var(--x, 0));
}
@keyframes tk-converge {
0% { opacity: 0; transform: translateX(var(--x, 0)); }
35%, 80% { opacity: 1; transform: translateX(0); }
100% { opacity: 0; transform: translateX(var(--x, 0)); }
}
.tk-word.is-demo .tk-ch { animation: tk-converge 2s ease-out infinite; }
08輪郭から塗りつぶしへ
文字がまず輪郭だけの状態で現れ、左から色が流し込まれて塗りつぶされた文字になります。下に -webkit-text-stroke の輪郭レイヤーを敷き、その上に重ねた色付きの文字を clip-path で左から開いていく構成です。
.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);
}
@keyframes fl-wipe {
0%, 10% { clip-path: inset(0 100% 0 0); }
70%, 90% { clip-path: inset(0 0 0 0); }
100% { clip-path: inset(0 100% 0 0); }
}
.fl-wrap.is-demo .fl-text--fill { animation: fl-wipe 2s ease-out infinite; }
09影を重ねて立体化
平らだった文字の背後に影の層が重なって厚みが生まれ、また平らに戻ります。1pxから4pxまでずらした4枚とぼかした1枚を text-shadow 1つの値にまとめてあるので、変わるのはプロパティ1つだけです。
.sh-text { text-shadow: none; }
@keyframes sh-extrude {
0%, 15% { text-shadow: none; }
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);
}
100% { text-shadow: none; }
}
.sh-text.is-demo { animation: sh-extrude 2s ease-out infinite; }
css テキスト アニメーション はどこで壊れるか
最初につまずいたのは08番でした。輪郭の文字の上に色付きの文字をただ重ねただけだったので、絶対配置で常に前面にある色付きレイヤーが輪郭を完全に覆ってしまい、輪郭から塗られていく過程が画面に一度も出てきませんでした。外から見るとループは正常に回っているように見えましたが、収録したフレームを1枚ずつ開くと、3枚目には色付きの文字が1つだけ残り、あとはほぼ空の画面でした。transform だけで済ませたくて、背景と同じ色の覆いを滑らせて隠す作りにしていたのですが、その覆いこそが見せたい段階を消していたわけです。効いたのは逆の手で、色付きレイヤー自身に clip-path をかけて左から広げると、輪郭の段階がようやく見えるようになりました。直したあとのソースは9つ分そろえてあり、解凍パスワード jagvrc78 で開く zip にはプレーンなCSS版とReact版の両方が入っています。
アクセシビリティ — 9つが reduced-motion で残すもの
prefers-reduced-motion: reduce を有効にしている人には、9つとも動きを止めて最終状態のまま表示されます。SCSSの @keyframes で動く8つは、その最終状態を !important で固定してあります。単語は上がりきり、斜めのマスクは文字の外へ抜けきり、字間は詰まり、輪郭は塗られ、影は重なった姿です。動きを切っていても空の画面ではなく結果が見えます。06番だけはCSSアニメーションを持たないので、スクリプトの中で matchMedia を確認し、文字を混ぜずに完成した単語をそのまま表示します。
文字が順番に現れる例をもっと見たい場合は CSS 文字 アニメーション 9選 が参考になり、全体の一覧は CSSのみカテゴリ にまとめてあります。08番で使った書き方は MDN の clip-path の解説で確認できます。
FAQ — よくある質問
テキストのアニメーションにJavaScriptは必要ですか
ほとんどは不要です。9つのうち8つはSCSSの @keyframes だけで動き、06番のスクランブルデコードだけが文字の内容を入れ替える必要があるため、短いJavaScriptを使います。
見出しには01番と04番のどちらが向いていますか
1行から2行の短い見出しなら、01番のように単語ごとに区切るほうが自然に読めます。2行から3行にわたる宣言文やミッションステートメントなら、行単位で左右から入る04番のほうが、単語単位では出せないリズムを生みます。
zip の中の vanilla と React でコードは違いますか
見た目の動きは両方とも同じです。違うのはきっかけで、vanilla版は is-demo クラスで強制的に繰り返し再生させているのに対し、React版は IntersectionObserver で画面に入った瞬間に一度だけ再生します。