CSS 文字 アニメーション 9選 — 一行でコピペOK
css 文字 アニメーションは、JavaScriptを使わずCSSのtransform・opacity・filterだけで見出しや文言を動かす小さな演出です。この記事は9種類を選び、6〜25行のコードに絞って実装を確かめました。冒頭のグリッドは記事を開いた瞬間からループ再生され、クリックしても止まりません。
自動再生 · タイルを押すとその項目へ · すべて1つのzip
9つの並び順は派手さではなく、文字が画面でたどる時間の流れに合わせています。まず現れる瞬間を担う5つ(01〜05)を置き、現れ終わったあとも回り続ける待機演出(06)を挟んで、すでに見えている文言へ視線だけを引き戻す2つ(07・08)に進み、最後は異常事態を示す崩れ(09)で締めくくりました。01・05・07・08・09は文字全体を一つの塊として動かし、02・03・04・06だけは<span>で一文字ずつ分けて時差を付けています。
01マスクの奥から立ち上がる文字
見出しを包む要素にoverflow: hiddenをかけ、内側の.mask__textだけをtransform: translateYで115%下の位置から0まで押し上げると、箱そのものは動かさずに文字だけがカーテンの奥から現れます。ヒーロー見出しの初回登場や、スクロールで現れるセクション見出しに向いています。
.mask {
overflow: hidden; // 箱の外に出た分は隠れる
max-width: min(380px, 90vw);
}
.mask__text {
color: $color;
animation: mask-up 2s $ease-spring infinite;
}
@keyframes mask-up {
0%, 4% { transform: translateY(115%); }
30%, 78% { transform: translateY(0); }
96%, 100% { transform: translateY(115%); }
}
02一文字ずつ弾んで着地する見出し
見出しの文字を一文字ずつspanへ分けてstyle="--i:N"を付与し、animation-delay: calc(var(--i) * 40ms)の一行だけで着地の時差を作ります。ロゴのイントロやランディングページのヒーロータイトルに向いています。
.letters span {
display: inline-block;
animation: letter-bounce 2s $ease-pop infinite;
animation-delay: calc(var(--i) * 40ms);
}
@keyframes letter-bounce {
0%, 4% { opacity: 0; transform: translateY(22px); }
22%, 70% { opacity: 1; transform: translateY(0); }
88%, 100% { opacity: 0; transform: translateY(-14px); }
}
03回転しながら定位置に収まる文字
文字ごとに--iで90ms刻みの時差を付け、rotateを-25度から0度へ、opacityを0から1へ同時に遷移させると、斜めに飛び込んだ文字が一文字ずつ真っすぐに収まって見えます。バッジやタグのような短いラベルの登場に向いています。
.rot span {
animation: rotate-in 2s $ease-out infinite;
animation-delay: calc(var(--i) * 90ms);
}
@keyframes rotate-in {
0%, 4% { opacity: 0; transform: rotate(-25deg); }
26%, 72% { opacity: 1; transform: rotate(0deg); }
90%, 100% { opacity: 0; transform: rotate(18deg); }
}
043D反転で切り替わる数字
親要素にperspectiveを与え、子のspanにbackface-visibility: hiddenを付けたうえでrotateXを90度から0度へ回すと、カードが手前に倒れ込むように文字が反転して現れます。このデモでは₩290という価格表示を例に、金額が更新された瞬間をはっきり見せています。
.flip {
perspective: 480px;
}
.flip span {
backface-visibility: hidden;
animation: flip-in .6s $ease-spring infinite;
animation-delay: calc(var(--i) * 70ms);
}
@keyframes flip-in {
0%, 4% { opacity: 0; transform: rotateX(90deg); }
30%, 74% { opacity: 1; transform: rotateX(0deg); }
94%, 100% { opacity: 0; transform: rotateX(-90deg); }
}
05ぼかしから輪郭が合う文字
filter: blurの値を14pxから0へ、opacityを0から1へ同時に遷移させるだけで、ピントが合っていくように文字が輪郭を取り戻します。他の項目と違いtransformを一切使わないため、位置は登場中もまったく揺れません。
.blur {
animation: blur-focus 2s $ease-out infinite;
}
@keyframes blur-focus {
0%, 4% { opacity: 0; filter: blur(14px); }
30%, 76% { opacity: 1; filter: blur(0); }
96%, 100% { opacity: 0; filter: blur(14px); }
}
06途切れず波打つ待機テキスト
文字ごとに90msずつ時差を付けた同じkeyframesでtranslateYを0と-16pxの間で往復させると、登場も退場もない常時ループの波が生まれます。読み込み待ちの案内文や、空の状態を飾る文言にちょうど良い密度です。
.wave span {
animation: wave-up 2s $ease-out infinite;
animation-delay: calc(var(--i) * 90ms);
}
@keyframes wave-up {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-16px); }
}
07一瞬だけ視線を引く小さな揺れ
rotateとtranslateXをたった1〜2度・2pxだけ動かす短い往復を、ループの66〜90%区間だけに集めて残りは完全に静止させています。新着バッジの横の案内文や、割引ラベルへ視線を誘導したいときに使います。
.wiggle {
animation: wiggle 2s $ease-out infinite;
}
@keyframes wiggle {
0%, 60%, 100% { transform: rotate(0) translateX(0); }
66% { transform: rotate(-2deg) translateX(-2px); }
72% { transform: rotate(2deg) translateX(2px); }
78% { transform: rotate(-2deg) translateX(-2px); }
84% { transform: rotate(2deg) translateX(2px); }
90% { transform: rotate(0) translateX(0); }
}
08二色の境界で塗り分けるスイープ
同じ文言を二重に重ね、下の層は固定色のまま、上の層だけclip-pathを左端から右端へ広げるkeyframesで拭き取ると、グラデーションではなくはっきりした二色の境界で色が塗られたように見えます。料金表のハイライトやキャンペーンコピーの強調に向いています。
.sweep__top {
position: absolute;
inset: 0;
animation: sweep-wipe 2s $ease-spring infinite;
}
@keyframes sweep-wipe {
0%, 4% { clip-path: inset(0 100% 0 0); }
40%, 62% { clip-path: inset(0 0 0 0); }
96%, 100% { clip-path: inset(0 100% 0 0); }
}
09二重の色ずれで崩れるグリッチ
同じ文言を色違いで二枚重ね、mix-blend-mode: screenとclip-path、短いtranslateXのずれを組み合わせて0〜24%と50〜66%の二区間だけ画面を乱します。エラーや警告のタイトル、ゲーマー・テック系ブランドのロゴ演出に使っています。
.glitch__r, .glitch__b {
position: absolute;
inset: 0;
mix-blend-mode: screen;
}
.glitch__r { animation: glitch-r 2s linear infinite; }
@keyframes glitch-r {
0% { transform: translateX(0); clip-path: inset(0 0 0 0); }
8% { transform: translateX(-6px); clip-path: inset(12% 0 58% 0); }
16% { transform: translateX(4px); clip-path: inset(48% 0 14% 0); }
24% { transform: translateX(0); clip-path: inset(0 0 0 0); }
50% { transform: translateX(-4px); clip-path: inset(20% 0 40% 0); }
58% { transform: translateX(5px); clip-path: inset(55% 0 8% 0); }
66% { transform: translateX(0); clip-path: inset(0 0 0 0); }
}
どこで壊れるか — 落とし穴
--iとcalc()で時差を計算する4つ(02・03・04・06)は、時差の計算を親要素ではなく文字自身のanimation-delayに持たせています。文字数が増えてもSCSSを触らずに済む代わりに、親の要素側にopacityのトランジションを別で足すと、そのトランジションは--iの値を知らないため全文字が同時に切り替わってしまいます。時差は必ず文字自身のanimationに持たせてください。
二つ目は明るさです。09のグリッチはmix-blend-mode: screenで赤と青の二層を重ねて色が分離したように見せていますが、MDNのmix-blend-modeの解説にある通りscreenは背景が明るいほど二色を白く溶かして消してしまいます。だからこそ09は暗い舞台(ink)にだけ置き、明るい舞台へ移すならscreenの代わりにdifferenceか不透明度のある単色レイヤーへ変える必要があります。実際に計測すると、06の波は24フレーム中23フレームで変化が拾え、07の小さな揺れは7フレームしか拾えませんでした。この差まで踏まえて整えた9本のzipを開く合言葉は97me82wyで、下のカードから取り出せます。
07のように短い区間だけに動きを詰め込む演出は、スクロールを速くめくる人には見えないまま通り過ぎてしまうことがあります。画面に入った瞬間に一度だけ再生して終えるより、今回のように2秒周期で繰り返すか、登場直後の短い時間にまとめて再生したほうが見落とされません。
アクセシビリティ
prefers-reduced-motion: reduceでは、9つとも最終状態へ即座に切り替わり、要素そのものは消えません。01マスクと05ぼかしはopacity 1・transformやfilterなしでそのまま見え、02・03・04・06のように文字ごとにspanへ分けた項目は全てのspanをopacity 1・transformなしに揃えます。07の揺れと09のグリッチのように情報を運ばず視線を引くためだけの動きはanimation: noneで完全に止め、08の二色スイープのように色そのものが情報を運ぶ項目はclip-pathを最初から0に固定して最終色だけを出しています。詳しい挙動はMDNのprefers-reduced-motionの項目で確認できます。
@media (prefers-reduced-motion: reduce) {
.mask__text, .blur, .wiggle { animation: none; transform: none; filter: none; opacity: 1; }
.letters span, .rot span, .flip span, .wave span { animation: none; opacity: 1; transform: none; }
.sweep__top { animation: none; clip-path: inset(0 0 0 0); }
.glitch__r, .glitch__b { animation: none; opacity: 0; }
}
文字以外のCSSアニメーションはこのカテゴリーに集めてあり、このブログの検証方法はこのプロフィールにまとめています。
FAQ
CSSだけで文字アニメーションを作れますか?
はい、9つ全てJavaScriptなしでCSSのkeyframesだけで動いています。文字ごとに分けた02・03・04・06もspanタグを手で並べて--iの値を振るだけで、別途スクリプトで文字を切り分ける処理は要りません。
見出しの文字数が変わったら何を直せばいいですか?
spanの数を増減させ、--iの値を順番に振り直すだけで足ります。animation-delay: calc(var(--i) * 間隔)の一行が文字数に関係なく時差を計算するので、keyframesやSCSS側は変更しなくて済みます。
再生速度を変えるにはどこを直しますか?
各項目のSCSS冒頭にある$duration相当の値を一つ変えるだけです。02・03・04・06のように--iを使う項目は時差の間隔も別の数値として並んでいるので、全体の速さと文字間の時差を別々に調整できます。