パルス通知バッジ

アイコン右上のカウントを波紋とポップで強調する通知バッジ。更新時のアテンション喚起に使えます。

#css#javascript#badge#notification

ライブデモ

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

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

HTML
<!-- Sakura:ファンサイトのニュース通知バッジ -->
<section class="pb-stage">
  <header class="pb-bar">
    <div class="pb-brand">🌸 Sakura <span class="pb-brand-sub">OFFICIAL FANCLUB</span></div>

    <nav class="pb-icons">
      <!-- 通知ベル:右上にパルスするカウントバッジ -->
      <button class="pb-icon" id="pbBell" type="button" aria-label="お知らせ">
        🔔
        <span class="pb-badge" id="pbBadge" data-count="3">3</span>
      </button>
      <button class="pb-icon" type="button" aria-label="メニュー">☰</button>
    </nav>
  </header>

  <div class="pb-body">
    <p class="pb-section">NEWS</p>
    <ul class="pb-news" id="pbNews">
      <li class="pb-news__item"><span class="pb-new">NEW</span> 全国ツアー「Bloom!」追加公演が決定</li>
      <li class="pb-news__item"><span class="pb-new">NEW</span> 新曲「春一番デイズ」配信スタート</li>
      <li class="pb-news__item"><span class="pb-new">NEW</span> 桜井みお ソロ写真集 予約受付中</li>
    </ul>
    <button class="pb-add" id="pbAdd" type="button">+ 新着お知らせを受け取る</button>
  </div>
</section>
CSS
/* Sakura:ニュース通知のパルスバッジ */
:root {
  --pink: #ffd1e0;
  --hot: #ff6fa5;
  --gray: #f2f3f5;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  height: 400px;
  font-family: "Hiragino Kaku Gothic ProN", "Yu Gothic UI", system-ui, sans-serif;
  background: linear-gradient(180deg, #fff6fa 0%, var(--pink) 100%);
  color: #5a3b48;
}

.pb-stage { height: 400px; display: flex; flex-direction: column; }

.pb-bar {
  display: flex; align-items: center; justify-content: space-between;
  padding: 14px 20px;
  background: #fff;
  box-shadow: 0 6px 18px -10px rgba(214, 86, 132, 0.4);
}
.pb-brand { font-size: 16px; font-weight: 800; letter-spacing: 0.04em; color: var(--hot); }
.pb-brand-sub { font-size: 9px; letter-spacing: 0.16em; color: #b89aa6; margin-left: 6px; }

.pb-icons { display: flex; gap: 8px; }
.pb-icon {
  position: relative;
  width: 40px; height: 40px;
  font-size: 18px;
  display: grid; place-items: center;
  border: none; border-radius: 12px; cursor: pointer;
  background: var(--gray);
  transition: transform 0.1s ease, background 0.2s ease;
}
.pb-icon:hover { background: #e7e9ec; }
.pb-icon:active { transform: scale(0.94); }

/* カウントバッジ本体 */
.pb-badge {
  position: absolute;
  top: -5px; right: -5px;
  min-width: 19px; height: 19px;
  padding: 0 5px;
  display: grid; place-items: center;
  font-size: 11px; font-weight: 800; line-height: 1;
  color: #fff;
  background: var(--hot);
  border: 2px solid #fff;
  border-radius: 999px;
  box-shadow: 0 2px 6px rgba(255, 111, 165, 0.6);
}
/* 波紋:バッジの後ろで広がって消える */
.pb-badge::before {
  content: "";
  position: absolute;
  inset: -2px;
  border-radius: 999px;
  background: var(--hot);
  z-index: -1;
  animation: pb-ripple 1.8s ease-out infinite;
}
@keyframes pb-ripple {
  0% { transform: scale(1); opacity: 0.55; }
  100% { transform: scale(2.4); opacity: 0; }
}
/* 数字が増えた瞬間のポップ */
.pb-badge.is-pop { animation: pb-pop 0.45s cubic-bezier(0.34, 1.56, 0.64, 1); }
@keyframes pb-pop {
  0% { transform: scale(0.5); }
  60% { transform: scale(1.3); }
  100% { transform: scale(1); }
}

.pb-body { flex: 1; padding: 16px 20px; }
.pb-section { margin: 0 0 10px; font-size: 11px; letter-spacing: 0.16em; color: var(--hot); font-weight: 700; }
.pb-news { list-style: none; margin: 0 0 14px; padding: 0; display: grid; gap: 8px; }
.pb-news__item {
  display: flex; align-items: center; gap: 8px;
  padding: 11px 13px;
  font-size: 13px;
  border-radius: 12px;
  background: #fff;
  box-shadow: 0 8px 18px -14px rgba(214, 86, 132, 0.5);
}
.pb-new {
  flex: none;
  font-size: 9px; font-weight: 800; letter-spacing: 0.06em;
  color: #fff; background: var(--hot);
  padding: 2px 7px; border-radius: 999px;
}

.pb-add {
  width: 100%;
  font: inherit; font-size: 12px; font-weight: 700;
  padding: 10px; border: none; border-radius: 12px; cursor: pointer;
  color: #fff; background: linear-gradient(135deg, #ff9ec0, var(--hot));
  box-shadow: 0 10px 22px -8px rgba(255, 111, 165, 0.7);
  transition: transform 0.1s ease;
}
.pb-add:active { transform: scale(0.98); }

@media (prefers-reduced-motion: reduce) {
  .pb-badge::before { animation: none; opacity: 0; }
  .pb-badge.is-pop { animation: none; }
}
JavaScript
// Sakura:新着お知らせでカウントを増やし、バッジをポップさせる
(() => {
  const badge = document.getElementById("pbBadge");
  const add = document.getElementById("pbAdd");
  const bell = document.getElementById("pbBell");
  const news = document.getElementById("pbNews");
  if (!badge) return; // null安全

  // ダミーの新着見出し
  const headlines = [
    "ファンミーティング2025 応募開始",
    "公式グッズ 新ラインナップ公開",
    "桜井みお 生配信が今夜21時",
    "限定壁紙プレゼント実施中",
  ];
  let pick = 0;

  // バッジの数値を更新してポップ演出
  const bump = (n) => {
    const cur = Number(badge.dataset.count) || 0;
    const next = Math.max(0, cur + n);
    badge.dataset.count = String(next);
    badge.textContent = next > 99 ? "99+" : String(next);
    badge.style.display = next > 0 ? "" : "none";
    badge.classList.remove("is-pop");
    void badge.offsetWidth; // リフローで再アニメ
    badge.classList.add("is-pop");
  };

  // 新着追加:カウント+1し、リスト先頭に挿入
  if (add) {
    add.addEventListener("click", () => {
      bump(1);
      if (news) {
        const li = document.createElement("li");
        li.className = "pb-news__item";
        li.innerHTML = `<span class="pb-new">NEW</span> ${headlines[pick % headlines.length]}`;
        news.prepend(li);
        pick++;
      }
    });
  }

  // ベルを押したら既読=カウント0に
  if (bell) {
    bell.addEventListener("click", () => {
      badge.dataset.count = "0";
      bump(0);
    });
  }
})();

コード

HTML
<!-- パルス通知バッジ:アイコンに付くカウントが波紋でアテンションを引く -->
<div class="pulse-stage">
  <div class="pulse-bar">
    <!-- ベル:未読あり -->
    <button class="pulse-icon" id="bellBtn" type="button" aria-label="通知">
      <svg viewBox="0 0 24 24" width="26" height="26" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
        <path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/>
        <path d="M13.7 21a2 2 0 0 1-3.4 0"/>
      </svg>
      <span class="pulse-badge" id="bellBadge" data-count="3">3</span>
    </button>

    <!-- メッセージ:未読あり -->
    <button class="pulse-icon" type="button" aria-label="メッセージ">
      <svg viewBox="0 0 24 24" width="26" height="26" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
        <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
      </svg>
      <span class="pulse-badge pulse-badge--dot" aria-label="新着あり"></span>
    </button>

    <!-- カート:多数 -->
    <button class="pulse-icon" type="button" aria-label="カート">
      <svg viewBox="0 0 24 24" width="26" height="26" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
        <circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/>
        <path d="M1 1h4l2.7 13.4a2 2 0 0 0 2 1.6h9.7a2 2 0 0 0 2-1.6L23 6H6"/>
      </svg>
      <span class="pulse-badge pulse-badge--amber" data-count="12">12</span>
    </button>
  </div>
  <button class="pulse-action" id="pulseAdd" type="button">+ 通知を増やす</button>
</div>
CSS
/* やわらかいライトUI風。ツールバーにアイコンを並べる */
* { box-sizing: border-box; }
body {
  margin: 0;
  min-height: 360px;
  display: grid;
  place-items: center;
  font-family: "Segoe UI", "Hiragino Sans", "Yu Gothic UI", system-ui, sans-serif;
  background: radial-gradient(120% 120% at 50% 10%, #f4f6ff 0%, #dde3f6 100%);
  color: #2a2f4a;
}
.pulse-stage { display: grid; gap: 22px; justify-items: center; }

.pulse-bar {
  display: flex; gap: 18px;
  padding: 14px 20px;
  border-radius: 18px;
  background: #fff;
  box-shadow: 0 18px 40px -16px rgba(40, 50, 110, .4);
}
.pulse-icon {
  position: relative;
  width: 52px; height: 52px;
  display: grid; place-items: center;
  border: none; border-radius: 14px;
  background: #eef1fb;
  color: #4a5280;
  cursor: pointer;
  transition: background .2s ease, transform .12s ease;
}
.pulse-icon:hover { background: #e2e7fb; }
.pulse-icon:active { transform: scale(.95); }

/* バッジ本体:右上にのせる */
.pulse-badge {
  position: absolute;
  top: -5px; right: -5px;
  min-width: 20px; height: 20px;
  padding: 0 5px;
  display: grid; place-items: center;
  border-radius: 999px;
  font-size: 11px; font-weight: 800; line-height: 1;
  color: #fff;
  background: #ff3b5c;
  border: 2px solid #fff;
}
/* 波紋:::before を拡大しながらフェード */
.pulse-badge::before {
  content: "";
  position: absolute; inset: -2px;
  border-radius: inherit;
  background: inherit;
  z-index: -1;
  animation: pulseRing 1.8s ease-out infinite;
}
@keyframes pulseRing {
  0%   { transform: scale(1);   opacity: .55; }
  70%  { transform: scale(2.1); opacity: 0; }
  100% { transform: scale(2.1); opacity: 0; }
}
/* 数値が増えた瞬間のポップ(JSで一時付与) */
.pulse-badge.bump { animation: pulseBump .42s cubic-bezier(.34,1.56,.64,1); }
@keyframes pulseBump {
  0% { transform: scale(1); }
  45% { transform: scale(1.45); }
  100% { transform: scale(1); }
}

/* ドットのみ版 */
.pulse-badge--dot { min-width: 12px; width: 12px; height: 12px; padding: 0; }
/* 琥珀色版 */
.pulse-badge--amber { background: #ff9d2e; }

.pulse-action {
  padding: 10px 18px;
  border: 1px solid rgba(60, 70, 130, .25);
  border-radius: 10px;
  background: #fff;
  color: #3a4276; font-size: 13px; font-weight: 600;
  cursor: pointer;
  box-shadow: 0 6px 16px -8px rgba(40, 50, 110, .5);
  transition: transform .12s ease;
}
.pulse-action:active { transform: scale(.97); }

@media (prefers-reduced-motion: reduce) {
  .pulse-badge::before { animation: none; opacity: 0; }
  .pulse-badge.bump { animation: none; }
}
JavaScript
// パルス通知バッジ:カウント更新時にポップ(bump)アニメを再付与
(() => {
  const addBtn = document.getElementById('pulseAdd');
  const bellBadge = document.getElementById('bellBadge');
  const bellBtn = document.getElementById('bellBtn');
  if (!bellBadge) return; // null安全

  // 数値バッジを n だけ増やし、bump を一度だけ再生
  const bump = (badge) => {
    badge.classList.remove('bump');
    void badge.offsetWidth; // リフローで再発火
    badge.classList.add('bump');
  };

  const setCount = (badge, value) => {
    const v = Math.max(0, value);
    badge.dataset.count = String(v);
    // 99超は 99+ 表記
    badge.textContent = v > 99 ? '99+' : String(v);
    bump(badge);
  };

  // 「増やす」ボタン:ベルのカウントを+1
  if (addBtn) {
    addBtn.addEventListener('click', () => {
      const cur = parseInt(bellBadge.dataset.count || '0', 10);
      setCount(bellBadge, cur + 1);
    });
  }

  // ベルをクリックで既読化(0に)
  if (bellBtn) {
    bellBtn.addEventListener('click', (e) => {
      if (e.target.closest('.pulse-action')) return;
      const cur = parseInt(bellBadge.dataset.count || '0', 10);
      if (cur > 0) setCount(bellBadge, 0);
      else setCount(bellBadge, 3); // トグルでデモ復帰
    });
  }
})();

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

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

# 追加してほしい効果
パルス通知バッジ(アニメーション & トランジション)
アイコン右上のカウントを波紋とポップで強調する通知バッジ。更新時のアテンション喚起に使えます。

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

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- パルス通知バッジ:アイコンに付くカウントが波紋でアテンションを引く -->
<div class="pulse-stage">
  <div class="pulse-bar">
    <!-- ベル:未読あり -->
    <button class="pulse-icon" id="bellBtn" type="button" aria-label="通知">
      <svg viewBox="0 0 24 24" width="26" height="26" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
        <path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/>
        <path d="M13.7 21a2 2 0 0 1-3.4 0"/>
      </svg>
      <span class="pulse-badge" id="bellBadge" data-count="3">3</span>
    </button>

    <!-- メッセージ:未読あり -->
    <button class="pulse-icon" type="button" aria-label="メッセージ">
      <svg viewBox="0 0 24 24" width="26" height="26" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
        <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
      </svg>
      <span class="pulse-badge pulse-badge--dot" aria-label="新着あり"></span>
    </button>

    <!-- カート:多数 -->
    <button class="pulse-icon" type="button" aria-label="カート">
      <svg viewBox="0 0 24 24" width="26" height="26" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
        <circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/>
        <path d="M1 1h4l2.7 13.4a2 2 0 0 0 2 1.6h9.7a2 2 0 0 0 2-1.6L23 6H6"/>
      </svg>
      <span class="pulse-badge pulse-badge--amber" data-count="12">12</span>
    </button>
  </div>
  <button class="pulse-action" id="pulseAdd" type="button">+ 通知を増やす</button>
</div>

【CSS】
/* やわらかいライトUI風。ツールバーにアイコンを並べる */
* { box-sizing: border-box; }
body {
  margin: 0;
  min-height: 360px;
  display: grid;
  place-items: center;
  font-family: "Segoe UI", "Hiragino Sans", "Yu Gothic UI", system-ui, sans-serif;
  background: radial-gradient(120% 120% at 50% 10%, #f4f6ff 0%, #dde3f6 100%);
  color: #2a2f4a;
}
.pulse-stage { display: grid; gap: 22px; justify-items: center; }

.pulse-bar {
  display: flex; gap: 18px;
  padding: 14px 20px;
  border-radius: 18px;
  background: #fff;
  box-shadow: 0 18px 40px -16px rgba(40, 50, 110, .4);
}
.pulse-icon {
  position: relative;
  width: 52px; height: 52px;
  display: grid; place-items: center;
  border: none; border-radius: 14px;
  background: #eef1fb;
  color: #4a5280;
  cursor: pointer;
  transition: background .2s ease, transform .12s ease;
}
.pulse-icon:hover { background: #e2e7fb; }
.pulse-icon:active { transform: scale(.95); }

/* バッジ本体:右上にのせる */
.pulse-badge {
  position: absolute;
  top: -5px; right: -5px;
  min-width: 20px; height: 20px;
  padding: 0 5px;
  display: grid; place-items: center;
  border-radius: 999px;
  font-size: 11px; font-weight: 800; line-height: 1;
  color: #fff;
  background: #ff3b5c;
  border: 2px solid #fff;
}
/* 波紋:::before を拡大しながらフェード */
.pulse-badge::before {
  content: "";
  position: absolute; inset: -2px;
  border-radius: inherit;
  background: inherit;
  z-index: -1;
  animation: pulseRing 1.8s ease-out infinite;
}
@keyframes pulseRing {
  0%   { transform: scale(1);   opacity: .55; }
  70%  { transform: scale(2.1); opacity: 0; }
  100% { transform: scale(2.1); opacity: 0; }
}
/* 数値が増えた瞬間のポップ(JSで一時付与) */
.pulse-badge.bump { animation: pulseBump .42s cubic-bezier(.34,1.56,.64,1); }
@keyframes pulseBump {
  0% { transform: scale(1); }
  45% { transform: scale(1.45); }
  100% { transform: scale(1); }
}

/* ドットのみ版 */
.pulse-badge--dot { min-width: 12px; width: 12px; height: 12px; padding: 0; }
/* 琥珀色版 */
.pulse-badge--amber { background: #ff9d2e; }

.pulse-action {
  padding: 10px 18px;
  border: 1px solid rgba(60, 70, 130, .25);
  border-radius: 10px;
  background: #fff;
  color: #3a4276; font-size: 13px; font-weight: 600;
  cursor: pointer;
  box-shadow: 0 6px 16px -8px rgba(40, 50, 110, .5);
  transition: transform .12s ease;
}
.pulse-action:active { transform: scale(.97); }

@media (prefers-reduced-motion: reduce) {
  .pulse-badge::before { animation: none; opacity: 0; }
  .pulse-badge.bump { animation: none; }
}

【JavaScript】
// パルス通知バッジ:カウント更新時にポップ(bump)アニメを再付与
(() => {
  const addBtn = document.getElementById('pulseAdd');
  const bellBadge = document.getElementById('bellBadge');
  const bellBtn = document.getElementById('bellBtn');
  if (!bellBadge) return; // null安全

  // 数値バッジを n だけ増やし、bump を一度だけ再生
  const bump = (badge) => {
    badge.classList.remove('bump');
    void badge.offsetWidth; // リフローで再発火
    badge.classList.add('bump');
  };

  const setCount = (badge, value) => {
    const v = Math.max(0, value);
    badge.dataset.count = String(v);
    // 99超は 99+ 表記
    badge.textContent = v > 99 ? '99+' : String(v);
    bump(badge);
  };

  // 「増やす」ボタン:ベルのカウントを+1
  if (addBtn) {
    addBtn.addEventListener('click', () => {
      const cur = parseInt(bellBadge.dataset.count || '0', 10);
      setCount(bellBadge, cur + 1);
    });
  }

  // ベルをクリックで既読化(0に)
  if (bellBtn) {
    bellBtn.addEventListener('click', (e) => {
      if (e.target.closest('.pulse-action')) return;
      const cur = parseInt(bellBadge.dataset.count || '0', 10);
      if (cur > 0) setCount(bellBadge, 0);
      else setCount(bellBadge, 3); // トグルでデモ復帰
    });
  }
})();

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

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