可変フォント風ウェイトアニメ

JSで文字ごとに font-weight をsin波で連続変化させ、可変フォントのような波打つ太さ変化を再現します。動的なロゴやヘッダーに。

#js#css#animation

ライブデモ

使用例(お題: SaaS FlowDesk)

この技法を「SaaS FlowDesk」というテーマのダミーサイトで実際に使った例です。

HTML
<div class="page">
  <header class="nav">
    <div class="brand"><span class="logo"></span>FlowDesk</div>
    <nav class="links"><a>機能</a><a>料金</a><a class="btn">ログイン</a></nav>
  </header>

  <section class="hero">
    <p class="eyebrow">ALL-IN-ONE WORKSPACE</p>
    <h1 class="wave" data-text="FLOWDESK"></h1>
    <p class="lead">太さが波打つ、生きたブランドロゴ。<br>あなたのチームに、流れと勢いを。</p>
    <div class="metrics">
      <div class="m"><b>12,400+</b><span>導入チーム</span></div>
      <div class="m"><b>99.98%</b><span>稼働率</span></div>
      <div class="m"><b>4.9</b><span>満足度</span></div>
    </div>
  </section>
</div>
CSS
/* FlowDesk:太さが波打つロゴ見出しが主役 */
* { margin: 0; padding: 0; box-sizing: border-box; }

body {
  font-family: "Segoe UI", system-ui, sans-serif;
  background:
    radial-gradient(800px 380px at 50% -15%, #1a2c57 0%, transparent 60%),
    linear-gradient(160deg, #0f1b34 0%, #0b1326 100%);
  color: #e7ecf7;
  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: 700; font-size: 16px; }
.logo {
  width: 17px; height: 17px; border-radius: 6px;
  background: linear-gradient(135deg, #4f7cff, #8ab4ff);
  box-shadow: 0 0 10px rgba(79,124,255,0.6);
}
.links { display: flex; align-items: center; gap: 16px; }
.links a { font-size: 13px; color: #aeb9d4; cursor: pointer; }
.links .btn { color: #fff; background: #4f7cff; padding: 7px 14px; border-radius: 8px; font-weight: 600; }

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

/* 波打つロゴ本体:文字ごとにJSがfont-weightを変える */
.wave {
  margin-top: 14px;
  font-size: clamp(40px, 9vw, 76px);
  line-height: 1;
  letter-spacing: 0.04em;
  color: #fff;
  font-weight: 400;
}
.wave .ch {
  display: inline-block;
  /* 太さ変化をなめらかに */
  transition: font-weight 0.08s linear;
  background: linear-gradient(180deg, #ffffff, #aac2ff);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.lead { margin-top: 16px; font-size: 13.5px; line-height: 1.7; color: #aeb9d4; }

.metrics {
  margin-top: 22px;
  display: flex; gap: 14px; justify-content: center;
}
.m {
  background: rgba(79,124,255,0.08);
  border: 1px solid #233459;
  border-radius: 12px;
  padding: 12px 18px; min-width: 92px;
}
.m b { display: block; font-size: 19px; color: #8ab4ff; }
.m span { font-size: 11px; color: #8593b5; }
JavaScript
// 文字ごとにfont-weightをsin波で連続変化させる
(function () {
  const el = document.querySelector('.wave');
  if (!el) return; // null安全

  const text = el.dataset.text || 'FLOWDESK';
  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  // 1文字ずつspan化
  const spans = [...text].map((c) => {
    const s = document.createElement('span');
    s.className = 'ch';
    s.textContent = c;
    el.appendChild(s);
    return s;
  });

  // 控えめ設定では中庸の太さで固定
  if (reduce) { spans.forEach((s) => (s.style.fontWeight = 600)); return; }

  const MIN = 200, MAX = 900;
  let t = 0;

  function frame() {
    t += 0.06;
    spans.forEach((s, i) => {
      // 位相を文字ごとにずらして波を進める
      const wave = (Math.sin(t - i * 0.5) + 1) / 2; // 0..1
      s.style.fontWeight = Math.round(MIN + wave * (MAX - MIN));
    });
    requestAnimationFrame(frame);
  }
  frame();
})();

コード

HTML
<main class="stage">
  <p class="label">VARIABLE WEIGHT</p>
  <!-- JSが各文字を<span>化し、font-weightを波打たせる -->
  <h1 class="wave" data-text="Typography"></h1>
  <p class="note">font-weight を文字ごとに波形で変化</p>
</main>
CSS
* { margin: 0; padding: 0; box-sizing: border-box; }

body {
  min-height: 360px;
  display: grid;
  place-items: center;
  /* 明るくクリーンな背景 */
  background:
    radial-gradient(900px 500px at 50% 0%, #ffffff 0%, #eef1f7 70%, #e6eaf3 100%);
  font-family: "Segoe UI", "Helvetica Neue", system-ui, sans-serif;
  color: #14181f;
  overflow: hidden;
  padding: 24px;
}

.stage { text-align: center; }

.label {
  font-size: 12px;
  letter-spacing: 0.45em;
  font-weight: 600;
  color: #8a93a6;
  margin-bottom: 16px;
  padding-left: 0.45em;
}

.wave {
  font-size: clamp(44px, 11vw, 92px);
  line-height: 1.1;
  letter-spacing: 0.01em;
  /* システムフォントは可変軸が無いことが多いので、font-weightで近似 */
  white-space: nowrap;
}

/* 各文字。weightとスケールはJSが毎フレーム更新 */
.wave .g {
  display: inline-block;
  /* will-changeで重み変化のちらつきを軽減 */
  will-change: font-weight, transform;
  transition: none;
  /* 重み変化でにじむグラデの下線アクセント */
  background-image: linear-gradient(120deg, #6366f1, #ec4899);
  background-size: 100% 2px;
  background-repeat: no-repeat;
  background-position: 0 100%;
}

.note {
  margin-top: 28px;
  font-size: 12px;
  letter-spacing: 0.16em;
  color: #97a0b2;
}

@media (prefers-reduced-motion: reduce) {
  .wave .g { font-weight: 600 !important; transform: none !important; }
}
JavaScript
// 可変フォント風: font-weight を文字ごとに波形でアニメ
(function () {
  const el = document.querySelector('.wave');
  if (!el) return; // null安全

  const text = el.dataset.text || '';
  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  // 文字を<span>に分解
  const spans = [...text].map((char) => {
    const s = document.createElement('span');
    s.className = 'g';
    s.textContent = char;
    el.appendChild(s);
    return s;
  });

  if (reduce || spans.length === 0) return; // 静的表示で終了

  const MIN = 200, MAX = 900;   // ウェイト範囲
  const SPEED = 0.0028;         // 波の進む速さ
  const SPREAD = 0.55;          // 隣接文字との位相差

  let start = null;

  // requestAnimationFrameで滑らかに更新
  function frame(now) {
    if (start === null) start = now;
    const t = (now - start) * SPEED;

    spans.forEach((s, i) => {
      // sinで0..1の波を作り、文字位置で位相をずらす
      const wave = (Math.sin(t - i * SPREAD) + 1) / 2;
      const weight = Math.round(MIN + wave * (MAX - MIN));
      s.style.fontWeight = weight;
      // 重い文字をわずかに持ち上げて立体感
      s.style.transform = 'translateY(' + (-wave * 6).toFixed(2) + 'px)';
    });

    requestAnimationFrame(frame);
  }

  requestAnimationFrame(frame);
})();

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

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「可変フォント風ウェイトアニメ」の効果を追加してください。

# 追加してほしい効果
可変フォント風ウェイトアニメ(タイポグラフィ)
JSで文字ごとに font-weight をsin波で連続変化させ、可変フォントのような波打つ太さ変化を再現します。動的なロゴやヘッダーに。

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

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<main class="stage">
  <p class="label">VARIABLE WEIGHT</p>
  <!-- JSが各文字を<span>化し、font-weightを波打たせる -->
  <h1 class="wave" data-text="Typography"></h1>
  <p class="note">font-weight を文字ごとに波形で変化</p>
</main>

【CSS】
* { margin: 0; padding: 0; box-sizing: border-box; }

body {
  min-height: 360px;
  display: grid;
  place-items: center;
  /* 明るくクリーンな背景 */
  background:
    radial-gradient(900px 500px at 50% 0%, #ffffff 0%, #eef1f7 70%, #e6eaf3 100%);
  font-family: "Segoe UI", "Helvetica Neue", system-ui, sans-serif;
  color: #14181f;
  overflow: hidden;
  padding: 24px;
}

.stage { text-align: center; }

.label {
  font-size: 12px;
  letter-spacing: 0.45em;
  font-weight: 600;
  color: #8a93a6;
  margin-bottom: 16px;
  padding-left: 0.45em;
}

.wave {
  font-size: clamp(44px, 11vw, 92px);
  line-height: 1.1;
  letter-spacing: 0.01em;
  /* システムフォントは可変軸が無いことが多いので、font-weightで近似 */
  white-space: nowrap;
}

/* 各文字。weightとスケールはJSが毎フレーム更新 */
.wave .g {
  display: inline-block;
  /* will-changeで重み変化のちらつきを軽減 */
  will-change: font-weight, transform;
  transition: none;
  /* 重み変化でにじむグラデの下線アクセント */
  background-image: linear-gradient(120deg, #6366f1, #ec4899);
  background-size: 100% 2px;
  background-repeat: no-repeat;
  background-position: 0 100%;
}

.note {
  margin-top: 28px;
  font-size: 12px;
  letter-spacing: 0.16em;
  color: #97a0b2;
}

@media (prefers-reduced-motion: reduce) {
  .wave .g { font-weight: 600 !important; transform: none !important; }
}

【JavaScript】
// 可変フォント風: font-weight を文字ごとに波形でアニメ
(function () {
  const el = document.querySelector('.wave');
  if (!el) return; // null安全

  const text = el.dataset.text || '';
  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  // 文字を<span>に分解
  const spans = [...text].map((char) => {
    const s = document.createElement('span');
    s.className = 'g';
    s.textContent = char;
    el.appendChild(s);
    return s;
  });

  if (reduce || spans.length === 0) return; // 静的表示で終了

  const MIN = 200, MAX = 900;   // ウェイト範囲
  const SPEED = 0.0028;         // 波の進む速さ
  const SPREAD = 0.55;          // 隣接文字との位相差

  let start = null;

  // requestAnimationFrameで滑らかに更新
  function frame(now) {
    if (start === null) start = now;
    const t = (now - start) * SPEED;

    spans.forEach((s, i) => {
      // sinで0..1の波を作り、文字位置で位相をずらす
      const wave = (Math.sin(t - i * SPREAD) + 1) / 2;
      const weight = Math.round(MIN + wave * (MAX - MIN));
      s.style.fontWeight = weight;
      // 重い文字をわずかに持ち上げて立体感
      s.style.transform = 'translateY(' + (-wave * 6).toFixed(2) + 'px)';
    });

    requestAnimationFrame(frame);
  }

  requestAnimationFrame(frame);
})();

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

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