Remotionでキネティックタイポグラフィを作る完全ガイド
Remotionでキネティックタイポグラフィを作る完全ガイド
文字が踊り、単語が飛び出し、見出しが生命を持つように動く——キネティックタイポグラフィは、現代のモーションデザインにおいて最も視線を引きつける表現のひとつです。製品ローンチ動画、SNSリール、説明動画のタイトルシーケンスまで、動くテキストは静止したテキストとは比べものにならないほどの注目を集めます。
Remotionはキネティックタイポグラフィに特に向いています。その理由は、テキストアニメーションが本質的にプログラミングの問題だからです。文字列を分割し、各要素に時間オフセットを与え、物理演算ベースのトランスフォームを適用する——従来のモーショングラフィックスツールで何十ものキーフレームが必要だった作業が、TypeScriptで数十行に収まります。
この記事では、キネティックタイポグラフィの基礎となる文字分割から、単語単位アニメーション、スプリング物理演算を使ったレタースケール・透明度・translateYのパターン、さらに @remotion/noise による有機的な動きの付け方まで、体系的に解説します。
キネティックタイポグラフィとは何か
キネティックタイポグラフィとは、動き自体がメッセージの一部であるアニメーションテキストのことです。速くてエネルギッシュな動き、柔らかく漂う動き、弾力のある遊び心ある動き——アニメーションのスタイルは、言葉の意味と同様にトーンを伝えます。
代表的なパターンを整理します。
- エントランスアニメーション — 文字や単語が下・上・透明から登場する
- スタガードリビール — 各文字や単語が少しずつ遅れて登場するカスケード効果
- スケールパルス — 重要な単語が一瞬大きくなり存在感を強調する
- 文字ごとの物理演算 — 各文字が独立したスプリング粒子として振る舞う
- 有機的ホバリング — アニメーション後も文字が微妙に揺れ続ける
最大の特徴は、各テキスト要素を個別に制御できることです。段落全体をひとつのユニットとして動かすのではなく、文字・音節・単語という「アトム」に分解して、それぞれを独立してアニメーションさせます。
文字分割の基盤
文字単位アニメーションの基礎は、文字列を個々の文字の配列に分割し、それぞれを独自の要素として描画することです。
const text = 'Hello';
const chars = text.split('');
// ['H', 'e', 'l', 'l', 'o']
各文字を display: inline-block の <span> で囲みます。これは非常に重要なポイントです。inline 要素には transform が効かないため、translateY、scale、rotate を適用するには inline-block または block が必須です。
export const KineticTitle: React.FC = () => {
const text = 'こんにちは';
const chars = text.split('');
return (
<div
style={{
fontFamily: '"Yu Gothic", "Hiragino Kaku Gothic ProN", "Noto Sans JP", sans-serif',
fontSize: 96,
fontWeight: 700,
color: '#ffffff',
display: 'flex',
}}
>
{chars.map((char, index) => (
<span
key={index}
style={{ display: 'inline-block' }}
>
{char === ' ' ? ' ' : char}
</span>
))}
</div>
);
};
半角スペースを ' '(ノーブレークスペース)に置換しているのは、inline-block 内の通常スペースが折り畳まれるためです。
<Sequence> を使った文字単位エントランス
<Sequence> はRemotionでコンテンツに時間オフセットを設けるための仕組みです。各文字を from オフセットをインデックスに基づいてずらした <Sequence> で囲むことで、自然なカスケードエントランスが実現します。
import { Sequence, useCurrentFrame, spring, interpolate, useVideoConfig } from 'remotion';
const Character: React.FC<{ char: string }> = ({ char }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const enter = spring({
frame,
fps,
config: { mass: 0.5, stiffness: 200, damping: 14 },
});
const translateY = interpolate(enter, [0, 1], [40, 0]);
const opacity = interpolate(enter, [0, 1], [0, 1], {
extrapolateRight: 'clamp',
});
return (
<span
style={{
display: 'inline-block',
transform: `translateY(${translateY}px)`,
opacity,
}}
>
{char === ' ' ? ' ' : char}
</span>
);
};
export const StaggeredTitle: React.FC = () => {
const text = 'Remotion';
const chars = text.split('');
const staggerFrames = 3; // 文字ごとに3フレームずらす
return (
<div
style={{
fontFamily: '-apple-system, "Segoe UI", Roboto, sans-serif',
fontSize: 120,
fontWeight: 800,
color: '#ffffff',
display: 'flex',
}}
>
{chars.map((char, index) => (
<Sequence key={index} from={index * staggerFrames} layout="none">
<Character char={char} />
</Sequence>
))}
</div>
);
};
layout="none" プロップを <Sequence> に付けることで、ラッピング <div> を描画しないようにします。これにより文字の <span> がインライン配置を保ちます。
各 <Sequence from={index * staggerFrames}> はシーケンスの開始フレームを決めます。シーケンス内部では useCurrentFrame() がシーケンス開始時を 0 として返すため、スプリングは各文字で必ずフレーム0から動き出します。
単語単位アニメーション
長い文章や本文テキストの場合、文字単位より単語単位でアニメーションさせる方が読みやすく、視覚的に穏やかです。構造は同じですが、分割をスペースで行います。
const text = 'Reactで動画を作る';
const words = text.split(' ');
const staggerFrames = 6;
const Word: React.FC<{ word: string }> = ({ word }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const enter = spring({
frame,
fps,
config: { mass: 0.8, stiffness: 100, damping: 12 },
});
const translateY = interpolate(enter, [0, 1], [30, 0]);
const opacity = enter;
return (
<span
style={{
display: 'inline-block',
marginRight: '0.3em',
transform: `translateY(${translateY}px)`,
opacity,
}}
>
{word}
</span>
);
};
marginRight: '0.3em' で単語間のスペースを補完します。split(' ') でスペースを消費したため、手動でスペースを再現する必要があります。
スプリング設定の選び方
スプリングの設定がアニメーションの印象を大きく左右します。よく使う設定のパターンを紹介します。
スナッピーでエネルギッシュ
config: { mass: 0.4, stiffness: 300, damping: 14 }
文字がほぼ瞬時に定位置に収まります。力強さと速度を感じさせるタイトルに適しています。
弾力があって遊び心がある
config: { mass: 1, stiffness: 80, damping: 8 }
文字が定位置を超えてバウンドします。若年層向けや遊び心のあるコンテンツに合います。
スムーズでエレガント
config: { mass: 1, stiffness: 100, damping: 20 }
オーバーシュートなしで滑らかにイーズイン。プレミアムや編集的な文脈に適しています。
ドロップイン(落下+回転)
const enter = spring({ frame, fps, config: { mass: 0.6, stiffness: 150, damping: 12 } });
const translateY = interpolate(enter, [0, 1], [-60, 0]);
const rotate = interpolate(enter, [0, 1], [-15, 0]);
const opacity = interpolate(enter, [0, 1], [0, 1], { extrapolateRight: 'clamp' });
<span style={{
display: 'inline-block',
transform: `translateY(${translateY}px) rotate(${rotate}deg)`,
opacity,
}}>
{char}
</span>
スケール・透明度のエントランスパターン
translateY 以外でよく使うエントランストランスフォームをまとめます。
ゼロからスケールアップ
const scale = spring({ frame, fps, from: 0, to: 1, config: { mass: 0.6, stiffness: 200, damping: 15 } });
const opacity = interpolate(scale, [0, 1], [0, 1], { extrapolateRight: 'clamp' });
<span style={{ display: 'inline-block', transform: `scale(${scale})`, opacity }}>
{char}
</span>
大きな見出し文字でスケール効果が明確に読める場面に最適です。
ブラーフェードイン
const enter = spring({ frame, fps, config: { damping: 18, stiffness: 90 } });
const blurAmount = interpolate(enter, [0, 1], [8, 0]);
const opacity = interpolate(enter, [0, 1], [0, 1], { extrapolateRight: 'clamp' });
<span style={{
display: 'inline-block',
filter: `blur(${blurAmount}px)`,
opacity,
}}>
{char}
</span>
CSS filter: blur() はChromiumベースのRemotionレンダラーで綺麗に描画されます。
横スライドイン
const enter = spring({ frame, fps, config: { mass: 0.8, stiffness: 120, damping: 14 } });
const translateX = interpolate(enter, [0, 1], [-40, 0]);
const opacity = enter;
<span style={{ display: 'inline-block', transform: `translateX(${translateX}px)`, opacity }}>
{char}
</span>
複数のアニメーションを1つの文字で組み合わせる
高品質なキネティックタイポグラフィは、複数のトランスフォームとプロパティを同じ要素に同時に適用します。すべてのアニメーションが同じフレーム値に基づくため、自然に同期します。
const CharacterAdvanced: React.FC<{ char: string }> = ({ char }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// プライマリエントランス
const enter = spring({
frame,
fps,
config: { mass: 0.5, stiffness: 180, damping: 12 },
});
// セカンダリスプリング(少し遅い)——文字が「振り込む」感触
const settle = spring({
frame,
fps,
config: { mass: 1, stiffness: 60, damping: 10 },
});
const translateY = interpolate(enter, [0, 1], [50, 0]);
const rotate = interpolate(settle, [0, 1], [20, 0]);
const scale = interpolate(enter, [0, 1], [0.5, 1]);
const opacity = interpolate(enter, [0, 1], [0, 1], { extrapolateRight: 'clamp' });
return (
<span
style={{
display: 'inline-block',
transform: `translateY(${translateY}px) rotate(${rotate}deg) scale(${scale})`,
opacity,
transformOrigin: 'bottom center', // 下端を軸に回転
}}
>
{char}
</span>
);
};
transformOrigin: 'bottom center' を設定すると、文字が下端を軸に振り込むように動き、より自然に見えます。
@remotion/noise で有機的な動きを加える
スプリングアニメーションは決定論的で正確ですが、文字が生きているような微妙なランダム性がほしい場面もあります。@remotion/noise は時間に沿って滑らかに変化するパーリンノイズ値を提供します。
インストール:
npm install @remotion/noise
noise2D で各文字ごとに異なる位置オフセットを生成します。
import { noise2D } from '@remotion/noise';
import { useCurrentFrame } from 'remotion';
const FloatingChar: React.FC<{ char: string; index: number }> = ({ char, index }) => {
const frame = useCurrentFrame();
// 文字ごとに異なるシードを使用(indexをスケールして隣接文字が近いが異なる軌跡を持つようにする)
const noiseX = noise2D('x-drift', index * 0.3, frame * 0.02) * 6;
const noiseY = noise2D('y-drift', index * 0.3, frame * 0.02) * 4;
const noiseRotate = noise2D('rotate', index * 0.3, frame * 0.015) * 3;
return (
<span
style={{
display: 'inline-block',
transform: `translate(${noiseX}px, ${noiseY}px) rotate(${noiseRotate}deg)`,
}}
>
{char}
</span>
);
};
noise2D の第1引数はシード文字列です。x、y、回転で別々のシードを使うことで、すべてが同じ方向に動いてしまうのを防ぎます。第2引数は空間次元(インデックスをスケールダウン)、第3引数は時間次元です。frame * 0.02 のような小さな乗数で、滑らかでゆっくりした動きになります。
エントランス + フローティングの組み合わせ
最も効果的なアプローチは、スプリングエントランスと継続的なノイズ駆動のフローティングを組み合わせることです。
const AnimatedChar: React.FC<{ char: string; index: number }> = ({ char, index }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// スプリングエントランス
const enter = spring({
frame,
fps,
config: { mass: 0.5, stiffness: 200, damping: 14 },
});
const enterY = interpolate(enter, [0, 1], [40, 0]);
const opacity = interpolate(enter, [0, 1], [0, 1], { extrapolateRight: 'clamp' });
// 継続的なフロート(エントランス後も機能する)
const floatY = noise2D('float', index * 0.5, frame * 0.01) * 5;
const floatRotate = noise2D('rot', index * 0.5, frame * 0.008) * 2;
return (
<span
style={{
display: 'inline-block',
transform: `translateY(${enterY + floatY}px) rotate(${floatRotate}deg)`,
opacity,
}}
>
{char}
</span>
);
};
enterY + floatY の合計により、文字は下からスプリングで登場し、定位置に落ち着いた後も静かに揺れ続けます。
実践例:完成形のキネティックタイトルカード
すべてをまとめた本番品質のコンポーネントです。
import {
useCurrentFrame,
useVideoConfig,
spring,
interpolate,
Sequence,
} from 'remotion';
import { noise2D } from '@remotion/noise';
const Char: React.FC<{ char: string; index: number; total: number }> = ({
char,
index,
total,
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const enter = spring({
frame,
fps,
config: { mass: 0.5, stiffness: 220, damping: 13 },
});
const translateY = interpolate(enter, [0, 1], [60, 0]);
const scale = interpolate(enter, [0, 1], [0.7, 1]);
const opacity = interpolate(enter, [0, 1], [0, 1], { extrapolateRight: 'clamp' });
const floatY = noise2D('f', index / total, frame * 0.012) * 4;
return (
<span
style={{
display: 'inline-block',
transform: `translateY(${translateY + floatY}px) scale(${scale})`,
opacity,
color: '#ffffff',
fontFamily: '"Yu Gothic", "Hiragino Kaku Gothic ProN", "Noto Sans JP", sans-serif',
fontSize: 96,
fontWeight: 800,
lineHeight: 1.1,
letterSpacing: '0.04em',
}}
>
{char === ' ' ? ' ' : char}
</span>
);
};
export const KineticTitleCard: React.FC<{ title: string }> = ({ title }) => {
const chars = title.split('');
const stagger = 2;
return (
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: '#0a0a0a',
overflow: 'hidden',
}}
>
<div style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'center' }}>
{chars.map((char, i) => (
<Sequence key={i} from={i * stagger} layout="none">
<Char char={char} index={i} total={chars.length} />
</Sequence>
))}
</div>
</div>
);
};
パフォーマンスに関する注意
大量文字の同時アニメーション
200文字以上が一度に画面に表示され、すべてがフレームごとにスプリング計算を実行すると、Remotion Studioのプレビューでパフォーマンスが低下することがあります(最終レンダリングには影響しません——レンダリングは決定論的でCPUバウンドです)。長い本文テキストには単語や行単位でアニメーションさせることを推奨します。
Sequence に layout="none" を忘れない
layout="none" がないと各 <Sequence> がラッピング <div> を描画し、インラインテキストの流れが壊れます。インラインタイポグラフィのシーケンスには必ず付けてください。
ノイズの時間周波数を抑える
frame * 0.3 のような高い乗数は高周波のジッターを生み出します。0.05 以下に抑えると滑らかで有機的な動きになります。
よくある質問(FAQ)
Q: display: inline-block が必須な理由は何ですか?
CSS の transform プロパティは inline 要素に効果がありません。display: inline-block がないと、translateY、scale、rotate を style プロップに指定しても完全に無視されます。これはキネティックタイポグラフィが動かない最も多い原因の一つです。
Q: 複数行テキストで行ごとにカスケードさせるにはどうすればいいですか?
まずテキストを行に分割し、次に各行を文字に分割します。各行の開始フレームを lineIndex * lineStaggerFrames で計算し、各文字の開始フレームにその行のオフセットを加算します。行と文字の2段階のスタガーを組み合わせることで、自然な行ごとのカスケードが実現します。
Q: テキストをアクセシブルに保ったままアニメーションできますか?
文字を <span> で囲む構造はDOMのテキストを保持するため、スクリーンリーダーは読み取れます。ただし、単語をまたいだテキスト選択はスパンの境界でおかしな動作をする場合があります。動画出力では通常問題にならない範囲です。
Q: @remotion/noise は追加ライセンスが必要ですか?
不要です。@remotion/noise はRemotionパッケージエコシステムの一部であり、Remotion本体と同じライセンスに従います。商用プロジェクトではRemotionの商用ライセンスが必要であり、それはすべてのRemotionパッケージをカバーします。
Q: エントランスの逆、テキストを退場させるアニメーションはどう作りますか?
コンポジションの終端付近でフレームのカウントダウンを使います。useVideoConfig().durationInFrames - useCurrentFrame() の値を時間軸として、退場用のスプリングを別途作成するか、interpolate でフレーム範囲を設定してフェードアウトとtranslateYの逆アニメーションを適用します。
Q: スタガーの間隔は何フレームが適切ですか?
30fps動画の場合、タイトルテキスト(6〜8文字程度)では文字あたり2〜4フレームが自然に見えます。長い単語やフレーズでは1〜2フレームの方が全体のカスケードが短くまとまります。合計で1.5秒を超えるカスケードは間延びした印象になりがちです。
Q: <Sequence> を使わずに遅延を実装できますか?
spring() の delay パラメータを使う方法もあります。delay: index * staggerFrames を各文字のスプリングに渡すと、<Sequence> なしで同様のスタガーが実現します。ただし、この場合 useCurrentFrame() は <Sequence> でオフセットされないため、コンポジション全体のフレームが基準になります。<Sequence> を使う方が各文字のフレームが独立して管理されるため、より明確です。
まとめ
Remotionにおけるキネティックタイポグラフィは一貫したパターンに従います。
- テキストをアトム(文字または単語)に分割する
- 各アトムを
<span style={{ display: 'inline-block' }}>に、<Sequence from={index * stagger} layout="none">の中に配置する - 各アトムのコンポーネント内で
spring()を使ってエントランスの物理演算を駆動する interpolate()でスプリング出力をCSSプロパティにマッピングする@remotion/noiseでエントランス後の有機的な動きを追加する
プロ品質のキネティックタイポグラフィテンプレートは RenderComp で公開中 →
RenderCompのライブラリには、調整済みのスプリング設定で作られたキネティックタイトルカード、ワードリビールシーケンス、文字カスケードのローワーサードが揃っています。テキスト・カラー・タイミングをカスタマイズするだけで、すぐに本番品質のモーションが手に入ります。