キネティックタイポグラフィ

大きな単語が拡大・回転・スライドしながら次々と飛び込み、躍動的に入れ替わるループ演出をJSで実装します。語ごとにアニメ種別を切り替えて単調さを抑え、入場と退場をなめらかに繋ぎます。ヒーローやオープニングの強いキャッチに最適です。

#typography#kinetic#animation

ライブデモ

使用例(お題: アイドルグループ Sakura)

この技法を「アイドルグループ Sakura」というテーマのダミーサイトで実際に使った例です。

HTML
<div class="page">
  <header class="nav">
    <div class="brand"><span class="petal"></span>Sakura</div>
    <span class="tag">OPENING</span>
  </header>

  <section class="hero">
    <p class="eyebrow">2nd ANNIVERSARY</p>
    <div class="stage">
      <span class="word"></span>
    </div>
    <p class="lead">桜、2周年。<br>新しい季節へ、全力で駆けぬける。</p>
  </section>
</div>
CSS
/* Sakura:キネティックタイポのオープニングが主役 */
:root { --dur: 1.5; }
* { margin: 0; padding: 0; box-sizing: border-box; }

body {
  font-family: "Yu Gothic", "Segoe UI", sans-serif;
  background:
    radial-gradient(600px 340px at 50% -10%, #ffe3ec 0%, transparent 60%),
    linear-gradient(165deg, #fff5f8 0%, #ffe0ec 100%);
  color: #5a3a46;
  min-height: 400px;
  overflow: hidden;
}
.page { padding: 16px 26px; }

.nav { display: flex; align-items: center; justify-content: space-between; }
.brand { display: flex; align-items: center; gap: 9px; font-weight: 800; font-size: 16px; color: #e8638c; }
.petal { width: 14px; height: 14px; background: #ffd1e0; border-radius: 50% 0 50% 50%; transform: rotate(45deg); box-shadow: 0 0 8px rgba(232,99,140,0.4); }
.tag { font-size: 11px; color: #e8638c; letter-spacing: 0.3em; font-weight: 700; }

.hero { text-align: center; padding: 18px 6px 0; }
.eyebrow { font-size: 11px; letter-spacing: 0.3em; color: #c97a93; font-weight: 700; }

/* 単語が飛び込むステージ */
.stage {
  margin-top: 8px;
  height: 150px;
  display: grid; place-items: center;
  overflow: hidden;
}
.word {
  font-size: clamp(48px, 13vw, 100px);
  font-weight: 900; letter-spacing: 0.02em;
  color: #e8638c;
  text-shadow: 0 8px 20px rgba(232,99,140,0.3);
  will-change: transform, opacity;
}

/* 入場アニメ種別(語ごとに切り替え) */
.in-scale { animation: inScale var(--dur) cubic-bezier(0.2,0.8,0.2,1) both; }
.in-spin  { animation: inSpin  var(--dur) cubic-bezier(0.2,0.8,0.2,1) both; }
.in-slide { animation: inSlide var(--dur) cubic-bezier(0.2,0.8,0.2,1) both; }
.in-flip  { animation: inFlip  var(--dur) cubic-bezier(0.2,0.8,0.2,1) both; }

@keyframes inScale {
  0% { opacity: 0; transform: scale(0.2); }
  20%, 80% { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(1.6); }
}
@keyframes inSpin {
  0% { opacity: 0; transform: rotate(-90deg) scale(0.4); }
  20%, 80% { opacity: 1; transform: rotate(0) scale(1); }
  100% { opacity: 0; transform: rotate(40deg) scale(0.5); }
}
@keyframes inSlide {
  0% { opacity: 0; transform: translateX(-60%); }
  20%, 80% { opacity: 1; transform: translateX(0); }
  100% { opacity: 0; transform: translateX(60%); }
}
@keyframes inFlip {
  0% { opacity: 0; transform: perspective(500px) rotateX(90deg); }
  20%, 80% { opacity: 1; transform: perspective(500px) rotateX(0); }
  100% { opacity: 0; transform: perspective(500px) rotateX(-90deg); }
}

.lead { margin-top: 6px; font-size: 13.5px; line-height: 1.8; color: #8a6573; }

@media (prefers-reduced-motion: reduce) {
  .word { animation: none !important; opacity: 1 !important; transform: none !important; }
}
JavaScript
// 大きな語を躍動的に入れ替えるループ(オープニング)
(function () {
  const el = document.querySelector('.word');
  if (!el) return; // null安全

  // 桜の世界観に合わせた語
  const words = ['SAKURA', '満開', 'LIVE', '2nd', '咲け', 'GO!'];
  const types = ['in-scale', 'in-spin', 'in-slide', 'in-flip'];

  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  if (reduce) { el.textContent = 'SAKURA'; return; } // 控えめ設定は静的

  // CSS変数 --dur(秒)を1語の表示時間に
  const css = getComputedStyle(document.documentElement);
  const durSec = parseFloat(css.getPropertyValue('--dur')) || 1.5;
  const STEP = Math.max(700, durSec * 1000);

  let i = 0;
  function show() {
    const word = words[i % words.length];
    const type = types[i % types.length];
    el.className = 'word';
    el.textContent = word;
    void el.offsetWidth; // リフローで再トリガ
    el.classList.add(type);
    i += 1;
  }
  show();
  setInterval(show, STEP);
})();

コード

HTML
<main class="stage">
  <p class="kicker">KINETIC TYPE</p>
  <!-- 大きな単語が時間で躍動的に入れ替わるステージ -->
  <div class="kinetic" aria-live="polite">
    <span class="word" data-words></span>
  </div>
  <p class="caption">語が拡大・回転・スライドして躍動</p>
</main>
CSS
/* 全体リセットと暗い背景 */
* { margin: 0; padding: 0; box-sizing: border-box; }

:root {
  --bg-0: #0b0d18;
  --bg-1: #161a33;
  --accent: #ff5d8f;
  --accent-2: #5eead4;
  --ink: #f5f7ff;
  --dim: #8b93b8;
  --dur: 1.6s; /* 1語あたりの表示時間(JSと共有) */
}

body {
  min-height: 360px;
  display: grid;
  place-items: center;
  font-family: "Segoe UI", "Helvetica Neue", system-ui, sans-serif;
  /* 奥行きのある放射状+斜めグラデ背景 */
  background:
    radial-gradient(900px 420px at 50% -20%, #25305c 0%, transparent 60%),
    linear-gradient(160deg, var(--bg-0) 0%, var(--bg-1) 100%);
  color: var(--ink);
  overflow: hidden;
}

.stage { text-align: center; padding: 24px; width: 100%; }

.kicker {
  font-size: 12px;
  letter-spacing: 0.5em;
  font-weight: 700;
  color: var(--dim);
  margin-bottom: 18px;
  padding-left: 0.5em; /* レタースペース分の視覚補正 */
}

/* 大きな単語を中央に固定表示する枠 */
.kinetic {
  position: relative;
  height: clamp(80px, 22vw, 150px);
  display: grid;
  place-items: center;
  perspective: 700px; /* 回転に奥行きを与える */
}

.word {
  display: inline-block;
  font-size: clamp(48px, 13vw, 110px);
  font-weight: 900;
  line-height: 1;
  letter-spacing: -0.03em;
  white-space: nowrap;
  /* グラデで塗った躍動感のある文字 */
  background: linear-gradient(100deg, var(--accent) 0%, #b07bff 50%, var(--accent-2) 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  filter: drop-shadow(0 8px 26px rgba(176, 123, 255, 0.35));
  will-change: transform, opacity;
}

/* === 入場アニメ(JSがクラスを付け替える) === */
.word.in-scale  { animation: kScale  var(--dur) cubic-bezier(.2,.9,.2,1) both; }
.word.in-spin   { animation: kSpin   var(--dur) cubic-bezier(.2,.9,.2,1) both; }
.word.in-slide  { animation: kSlide  var(--dur) cubic-bezier(.2,.9,.2,1) both; }
.word.in-flip   { animation: kFlip   var(--dur) cubic-bezier(.2,.9,.2,1) both; }

/* 拡大しながら登場→少し縮んで退場 */
@keyframes kScale {
  0%   { opacity: 0; transform: scale(2.4); filter: blur(6px); }
  18%  { opacity: 1; transform: scale(1); filter: blur(0); }
  80%  { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(0.4); filter: blur(4px); }
}

/* 回転しながら登場→逆回転で退場 */
@keyframes kSpin {
  0%   { opacity: 0; transform: rotate(-90deg) scale(0.5); }
  20%  { opacity: 1; transform: rotate(0deg) scale(1); }
  80%  { opacity: 1; transform: rotate(0deg) scale(1); }
  100% { opacity: 0; transform: rotate(60deg) scale(0.6); }
}

/* 横から滑り込み→反対へ滑り去る */
@keyframes kSlide {
  0%   { opacity: 0; transform: translateX(120%) skewX(-12deg); }
  20%  { opacity: 1; transform: translateX(0) skewX(0); }
  80%  { opacity: 1; transform: translateX(0) skewX(0); }
  100% { opacity: 0; transform: translateX(-120%) skewX(12deg); }
}

/* X軸でめくれて登場→さらにめくれて退場 */
@keyframes kFlip {
  0%   { opacity: 0; transform: rotateX(90deg) translateY(20px); }
  20%  { opacity: 1; transform: rotateX(0) translateY(0); }
  80%  { opacity: 1; transform: rotateX(0) translateY(0); }
  100% { opacity: 0; transform: rotateX(-90deg) translateY(-20px); }
}

.caption {
  margin-top: 18px;
  font-size: 14px;
  color: var(--dim);
  letter-spacing: 0.04em;
}

/* モーション控えめ設定への配慮(任意) */
@media (prefers-reduced-motion: reduce) {
  .word { animation-duration: 0.01ms !important; }
}
JavaScript
// 大きな単語を時間で躍動的に入れ替えるループ
(function () {
  const el = document.querySelector('.word');
  if (!el) return; // null安全

  // 表示する単語と入場アニメ種別のローテーション
  const words = ['MOVE', 'PLAY', 'BOLD', 'FLOW', 'RISE', 'GO!'];
  const types = ['in-scale', 'in-spin', 'in-slide', 'in-flip'];

  // CSS変数 --dur(秒)を読み取り、1語の表示時間とする
  const css = getComputedStyle(document.documentElement);
  const durSec = parseFloat(css.getPropertyValue('--dur')) || 1.6;
  const STEP = Math.max(600, durSec * 1000); // ms

  let i = 0;

  function show() {
    const word = words[i % words.length];
    const type = types[i % types.length];

    // 一旦アニメをリセットしてから付け直す(再トリガ)
    el.className = 'word';
    el.textContent = word;
    // 強制リフローでアニメを確実に再生
    void el.offsetWidth;
    el.classList.add(type);

    i += 1;
  }

  show();
  // 表示時間ごとに次の語へ
  setInterval(show, STEP);
})();

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「キネティックタイポグラフィ」の効果を追加してください。

# 追加してほしい効果
キネティックタイポグラフィ(タイポグラフィ)
大きな単語が拡大・回転・スライドしながら次々と飛び込み、躍動的に入れ替わるループ演出をJSで実装します。語ごとにアニメ種別を切り替えて単調さを抑え、入場と退場をなめらかに繋ぎます。ヒーローやオープニングの強いキャッチに最適です。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<main class="stage">
  <p class="kicker">KINETIC TYPE</p>
  <!-- 大きな単語が時間で躍動的に入れ替わるステージ -->
  <div class="kinetic" aria-live="polite">
    <span class="word" data-words></span>
  </div>
  <p class="caption">語が拡大・回転・スライドして躍動</p>
</main>

【CSS】
/* 全体リセットと暗い背景 */
* { margin: 0; padding: 0; box-sizing: border-box; }

:root {
  --bg-0: #0b0d18;
  --bg-1: #161a33;
  --accent: #ff5d8f;
  --accent-2: #5eead4;
  --ink: #f5f7ff;
  --dim: #8b93b8;
  --dur: 1.6s; /* 1語あたりの表示時間(JSと共有) */
}

body {
  min-height: 360px;
  display: grid;
  place-items: center;
  font-family: "Segoe UI", "Helvetica Neue", system-ui, sans-serif;
  /* 奥行きのある放射状+斜めグラデ背景 */
  background:
    radial-gradient(900px 420px at 50% -20%, #25305c 0%, transparent 60%),
    linear-gradient(160deg, var(--bg-0) 0%, var(--bg-1) 100%);
  color: var(--ink);
  overflow: hidden;
}

.stage { text-align: center; padding: 24px; width: 100%; }

.kicker {
  font-size: 12px;
  letter-spacing: 0.5em;
  font-weight: 700;
  color: var(--dim);
  margin-bottom: 18px;
  padding-left: 0.5em; /* レタースペース分の視覚補正 */
}

/* 大きな単語を中央に固定表示する枠 */
.kinetic {
  position: relative;
  height: clamp(80px, 22vw, 150px);
  display: grid;
  place-items: center;
  perspective: 700px; /* 回転に奥行きを与える */
}

.word {
  display: inline-block;
  font-size: clamp(48px, 13vw, 110px);
  font-weight: 900;
  line-height: 1;
  letter-spacing: -0.03em;
  white-space: nowrap;
  /* グラデで塗った躍動感のある文字 */
  background: linear-gradient(100deg, var(--accent) 0%, #b07bff 50%, var(--accent-2) 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  filter: drop-shadow(0 8px 26px rgba(176, 123, 255, 0.35));
  will-change: transform, opacity;
}

/* === 入場アニメ(JSがクラスを付け替える) === */
.word.in-scale  { animation: kScale  var(--dur) cubic-bezier(.2,.9,.2,1) both; }
.word.in-spin   { animation: kSpin   var(--dur) cubic-bezier(.2,.9,.2,1) both; }
.word.in-slide  { animation: kSlide  var(--dur) cubic-bezier(.2,.9,.2,1) both; }
.word.in-flip   { animation: kFlip   var(--dur) cubic-bezier(.2,.9,.2,1) both; }

/* 拡大しながら登場→少し縮んで退場 */
@keyframes kScale {
  0%   { opacity: 0; transform: scale(2.4); filter: blur(6px); }
  18%  { opacity: 1; transform: scale(1); filter: blur(0); }
  80%  { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(0.4); filter: blur(4px); }
}

/* 回転しながら登場→逆回転で退場 */
@keyframes kSpin {
  0%   { opacity: 0; transform: rotate(-90deg) scale(0.5); }
  20%  { opacity: 1; transform: rotate(0deg) scale(1); }
  80%  { opacity: 1; transform: rotate(0deg) scale(1); }
  100% { opacity: 0; transform: rotate(60deg) scale(0.6); }
}

/* 横から滑り込み→反対へ滑り去る */
@keyframes kSlide {
  0%   { opacity: 0; transform: translateX(120%) skewX(-12deg); }
  20%  { opacity: 1; transform: translateX(0) skewX(0); }
  80%  { opacity: 1; transform: translateX(0) skewX(0); }
  100% { opacity: 0; transform: translateX(-120%) skewX(12deg); }
}

/* X軸でめくれて登場→さらにめくれて退場 */
@keyframes kFlip {
  0%   { opacity: 0; transform: rotateX(90deg) translateY(20px); }
  20%  { opacity: 1; transform: rotateX(0) translateY(0); }
  80%  { opacity: 1; transform: rotateX(0) translateY(0); }
  100% { opacity: 0; transform: rotateX(-90deg) translateY(-20px); }
}

.caption {
  margin-top: 18px;
  font-size: 14px;
  color: var(--dim);
  letter-spacing: 0.04em;
}

/* モーション控えめ設定への配慮(任意) */
@media (prefers-reduced-motion: reduce) {
  .word { animation-duration: 0.01ms !important; }
}

【JavaScript】
// 大きな単語を時間で躍動的に入れ替えるループ
(function () {
  const el = document.querySelector('.word');
  if (!el) return; // null安全

  // 表示する単語と入場アニメ種別のローテーション
  const words = ['MOVE', 'PLAY', 'BOLD', 'FLOW', 'RISE', 'GO!'];
  const types = ['in-scale', 'in-spin', 'in-slide', 'in-flip'];

  // CSS変数 --dur(秒)を読み取り、1語の表示時間とする
  const css = getComputedStyle(document.documentElement);
  const durSec = parseFloat(css.getPropertyValue('--dur')) || 1.6;
  const STEP = Math.max(600, durSec * 1000); // ms

  let i = 0;

  function show() {
    const word = words[i % words.length];
    const type = types[i % types.length];

    // 一旦アニメをリセットしてから付け直す(再トリガ)
    el.className = 'word';
    el.textContent = word;
    // 強制リフローでアニメを確実に再生
    void el.offsetWidth;
    el.classList.add(type);

    i += 1;
  }

  show();
  // 表示時間ごとに次の語へ
  setInterval(show, STEP);
})();

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。