見出しの文字が一文字ずつ浮き上がったり、バラバラに散ってから集まったりする演出は、個人サイトやブランドサイトでも当たり前に見かけるようになりました。多くの事例ではGSAPの拡張プラグインSplitTextが使われていますが、「テキストを1文字ずつ要素に分解し、個別にアニメーションさせる」という考え方さえ理解すれば、外部ライブラリなしでも再現できます。この記事ではその考え方を、バニラJavaScriptとCSSだけで実装する手順として初心者向けに解説します。
SplitTextが内部でやっていることを理解する
SplitTextのようなライブラリが特別な魔法を使っているわけではありません。やっていることは次の3ステップに集約されます。
1. 対象の要素からテキストを取得する 2. 元のテキストを空にする 3. 1文字ずつ<span>などの要素として作り直し、元の要素に戻す
つまり「文字ごとに動かせる箱(要素)」を用意しているだけです。箱さえあれば、あとはCSSのアニメーションやJavaScriptで好きに動かせます。この発想を押さえれば、ライブラリを増やしたくないプロジェクトでも同じ表現に到達できます。
依存なしで文字を分解する
まずは最小構成のコードを見てみましょう。以下のように、対象要素のテキストを1文字ずつspanに分解します。
<h1 class="heading">バラバラに動く見出し</h1>
const heading = document.querySelector(".heading");
const text = heading.textContent;
heading.textContent = "";
const frag = document.createDocumentFragment();
Array.from(text).forEach((char, i) => {
const span = document.createElement("span");
span.className = "char";
span.textContent = char;
span.style.setProperty("--i", i);
frag.appendChild(span);
});
heading.appendChild(frag);
ポイントはArray.from(text)を使っていることです。単純にtext.split("")を使うと、絵文字などが正しく1文字として扱われず表示が崩れることがあります。Array.fromはサロゲートペアを考慮して分割するため、日本語混じりの文章でも安全に扱えます。
また、各spanに--iというCSSカスタムプロパティで文字の順番を渡している点にも注目してください。これがあとで「ずらして動かす」ためのキーになります。
CSSだけでバラける動きを付ける
文字を分解できたら、あとはCSSを当てるだけです。.charにtransformとopacityのアニメーションを設定し、--iで開始タイミングをずらします。
.char {
display: inline-block;
opacity: 0;
transform: translateY(40px) rotate(8deg);
animation: char-in 0.6s cubic-bezier(.2,.8,.2,1) forwards;
animation-delay: calc(var(--i) * 40ms);
}
@keyframes char-in {
to {
opacity: 1;
transform: translateY(0) rotate(0deg);
}
}
animation-delayにcalc(var(--i) * 40ms)を指定するだけで、文字ごとに40ミリ秒ずつ遅れて動き出す「ドミノ倒し」のような演出が作れます。数値を大きくすればゆったりと、小さくすればキビキビした印象になります。文字が個別に飛び散ってから戻ってくる、次のような部品も根本の仕組みは同じです。
ランダム性を出してもっと有機的にする
すべての文字を同じ速度・同じ角度で動かすと、どうしても機械的な印象になります。もう一歩踏み込みたい場合は、CSSのcalc()だけに頼らずJavaScript側でランダムな値を計算し、インラインスタイルとして渡すとよいでしょう。
spans.forEach((span) => {
const angle = (Math.random() - 0.5) * 60;
const dist = 20 + Math.random() * 30;
span.style.setProperty("--angle", `${angle}deg`);
span.style.setProperty("--dist", `${dist}px`);
});
.char {
transform: translateY(var(--dist)) rotate(var(--angle));
}
こうすることで散らばる方向や距離が微妙に異なり、紙吹雪のような自然な揺らぎが生まれます。文字がジグザグに回転しながら並んでいく、次の部品も同じ発想の応用です。
つまずきやすいポイントと対策
実装時によくある失敗を3つ紹介します。
1. 改行やスペースが詰まって見える display:inline-blockを指定した文字は改行挙動を失いがちです。長文を分解する場合はwhite-space: normalを親要素に指定し、折り返せるようにしておくと安全です。
2. アニメーション後に元の状態へ戻ってしまう animation-fill-modeをforwardsにし忘れると、終了後に文字が初期状態(opacity:0など)へ巻き戻ります。上記のサンプルのように必ずforwardsを指定しましょう。
3. スクロールのたびに毎回再生されてしまう Intersection Observerで再生トリガーを作る場合は、一度再生した要素にフラグを立て、二度目以降は発火させないのが定石です。
まとめ
文字がバラバラに動くアニメーションは、ライブラリがなくても「1文字ずつ要素に分解し、順番や乱数でタイミングをずらす」という原則さえ理解すれば実装できます。まずはArray.fromによる安全な分解と、animation-delayによるずらしのテクニックから試してみてください。慣れてきたら乱数を取り入れ、さらに複雑な振り付けに挑戦するのもおすすめです。