通知バッジ

ベルアイコンに未読数を示す数値バブルを重ねた通知バッジ。ボタンで件数が増減するたびにポップして変化を伝え、9件を超えると「9+」と表記します。

#css#js#badge#notification

ライブデモ

コード

HTML
<!-- 通知バッジ: ベルに数値バブル。追加/既読ボタンで増減しポップ演出、9件超は「9+」表記 -->
<section class="panel" aria-label="通知バッジのデモ">
  <div class="bell" role="img" aria-label="通知、未読3件">
    <svg class="bell__icon" viewBox="0 0 24 24" width="30" height="30" fill="none" aria-hidden="true">
      <path d="M12 3a6 6 0 0 0-6 6v3.2c0 .58-.2 1.14-.57 1.59L4 15.5c-.7.85-.09 2.14 1 2.14h14c1.09 0 1.7-1.29 1-2.14l-1.43-1.71a2.5 2.5 0 0 1-.57-1.59V9a6 6 0 0 0-6-6Z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>
      <path d="M9.5 20a2.5 2.5 0 0 0 5 0" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
    </svg>
    <span class="bell__badge is-visible" id="bellBadge" aria-hidden="true">3</span>
  </div>
  <p class="hint" id="bellHint" role="status" aria-live="polite">未読 3件</p>
  <div class="controls">
    <button type="button" class="ctl ctl--add" id="addBtn">+ 通知を追加</button>
    <button type="button" class="ctl ctl--read" id="readBtn">− 既読にする</button>
  </div>
</section>
CSS
* { box-sizing: border-box; }
body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 24px;
  font-family: "Segoe UI", system-ui, sans-serif;
  background: linear-gradient(160deg, #0f1626 0%, #1a2440 55%, #222f52 100%);
}

.panel {
  width: min(320px, 92vw);
  display: grid;
  justify-items: center;
  gap: 18px;
  padding: 34px 26px 30px;
  border-radius: 22px;
  background: rgba(255, 255, 255, .06);
  border: 1px solid rgba(255, 255, 255, .12);
  box-shadow: 0 24px 50px -20px rgba(0, 0, 0, .55);
  backdrop-filter: blur(6px);
}

.bell {
  position: relative;
  width: 76px;
  height: 76px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: linear-gradient(160deg, #f8fafc, #dbe3f0);
  color: #1e293b;
  box-shadow: inset 0 2px 3px rgba(255, 255, 255, .8), 0 10px 24px -10px rgba(0, 0, 0, .6);
}

.bell__icon { display: block; }

/* 数値バブル: 通常は非表示、is-visibleで出現 */
.bell__badge {
  position: absolute;
  top: -4px;
  right: -6px;
  min-width: 24px;
  height: 24px;
  padding: 0 6px;
  border-radius: 999px;
  background: linear-gradient(155deg, #f87171, #dc2626);
  color: #fff;
  font-size: 12.5px;
  font-weight: 700;
  line-height: 24px;
  text-align: center;
  border: 2.5px solid #1a2440;
  box-shadow: 0 4px 10px rgba(220, 38, 38, .5);
  transform: scale(0);
  opacity: 0;
  transition: transform .25s cubic-bezier(.34, 1.56, .64, 1), opacity .2s ease;
}
.bell__badge.is-visible { transform: scale(1); opacity: 1; }

/* クリック時のポップ演出 */
.bell__badge.is-pop { animation: badge-pop .38s cubic-bezier(.34, 1.56, .64, 1); }
@keyframes badge-pop {
  0%   { transform: scale(1); }
  40%  { transform: scale(1.35); }
  100% { transform: scale(1); }
}

.hint {
  margin: 0;
  font-size: 13px;
  color: rgba(226, 232, 240, .75);
}

.controls {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  justify-content: center;
}

.ctl {
  appearance: none;
  border: 1px solid rgba(255, 255, 255, .18);
  background: rgba(255, 255, 255, .08);
  color: #e2e8f0;
  font-size: 12.5px;
  font-weight: 600;
  padding: 9px 14px;
  border-radius: 999px;
  cursor: pointer;
  transition: background .2s ease, transform .15s ease;
}
.ctl:hover { background: rgba(255, 255, 255, .16); }
.ctl:active { transform: scale(.96); }
.ctl:focus-visible { outline: 2px solid #93c5fd; outline-offset: 2px; }
.ctl:disabled { opacity: .4; cursor: not-allowed; }

.ctl--add { border-color: rgba(96, 165, 250, .5); }
.ctl--read { border-color: rgba(248, 113, 113, .4); }

@media (prefers-reduced-motion: reduce) {
  .bell__badge, .bell__badge.is-pop { animation: none !important; transition: none !important; }
}
JavaScript
// 通知バッジ: +/-で件数を増減し、変化のたびにバッジをポップさせる。9件超は「9+」表記、0件は非表示。
(() => {
  const bell = document.querySelector('.bell');
  const badge = document.getElementById('bellBadge');
  const hint = document.getElementById('bellHint');
  const addBtn = document.getElementById('addBtn');
  const readBtn = document.getElementById('readBtn');
  if (!bell || !badge || !hint || !addBtn || !readBtn) return; // null安全

  const MAX_DISPLAY = 9;
  let count = 3;

  const render = () => {
    const visible = count > 0;
    badge.textContent = count > MAX_DISPLAY ? `${MAX_DISPLAY}+` : String(count);
    badge.classList.toggle('is-visible', visible);
    bell.setAttribute('aria-label', visible ? `通知、未読${count}件` : '通知、未読はありません');
    hint.textContent = visible ? `未読 ${count}件` : '未読はありません';
    readBtn.disabled = count === 0;
  };

  const pop = () => {
    badge.classList.remove('is-pop');
    void badge.offsetWidth; // リフローを挟んでアニメーションを再生成
    badge.classList.add('is-pop');
  };

  addBtn.addEventListener('click', () => {
    count += 1;
    render();
    pop();
  });

  readBtn.addEventListener('click', () => {
    if (count === 0) return;
    count -= 1;
    render();
    pop();
  });

  render();
})();

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

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

# 追加してほしい効果
通知バッジ(バッジ & アバター)
ベルアイコンに未読数を示す数値バブルを重ねた通知バッジ。ボタンで件数が増減するたびにポップして変化を伝え、9件を超えると「9+」と表記します。

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

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 通知バッジ: ベルに数値バブル。追加/既読ボタンで増減しポップ演出、9件超は「9+」表記 -->
<section class="panel" aria-label="通知バッジのデモ">
  <div class="bell" role="img" aria-label="通知、未読3件">
    <svg class="bell__icon" viewBox="0 0 24 24" width="30" height="30" fill="none" aria-hidden="true">
      <path d="M12 3a6 6 0 0 0-6 6v3.2c0 .58-.2 1.14-.57 1.59L4 15.5c-.7.85-.09 2.14 1 2.14h14c1.09 0 1.7-1.29 1-2.14l-1.43-1.71a2.5 2.5 0 0 1-.57-1.59V9a6 6 0 0 0-6-6Z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>
      <path d="M9.5 20a2.5 2.5 0 0 0 5 0" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
    </svg>
    <span class="bell__badge is-visible" id="bellBadge" aria-hidden="true">3</span>
  </div>
  <p class="hint" id="bellHint" role="status" aria-live="polite">未読 3件</p>
  <div class="controls">
    <button type="button" class="ctl ctl--add" id="addBtn">+ 通知を追加</button>
    <button type="button" class="ctl ctl--read" id="readBtn">− 既読にする</button>
  </div>
</section>

【CSS】
* { box-sizing: border-box; }
body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 24px;
  font-family: "Segoe UI", system-ui, sans-serif;
  background: linear-gradient(160deg, #0f1626 0%, #1a2440 55%, #222f52 100%);
}

.panel {
  width: min(320px, 92vw);
  display: grid;
  justify-items: center;
  gap: 18px;
  padding: 34px 26px 30px;
  border-radius: 22px;
  background: rgba(255, 255, 255, .06);
  border: 1px solid rgba(255, 255, 255, .12);
  box-shadow: 0 24px 50px -20px rgba(0, 0, 0, .55);
  backdrop-filter: blur(6px);
}

.bell {
  position: relative;
  width: 76px;
  height: 76px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: linear-gradient(160deg, #f8fafc, #dbe3f0);
  color: #1e293b;
  box-shadow: inset 0 2px 3px rgba(255, 255, 255, .8), 0 10px 24px -10px rgba(0, 0, 0, .6);
}

.bell__icon { display: block; }

/* 数値バブル: 通常は非表示、is-visibleで出現 */
.bell__badge {
  position: absolute;
  top: -4px;
  right: -6px;
  min-width: 24px;
  height: 24px;
  padding: 0 6px;
  border-radius: 999px;
  background: linear-gradient(155deg, #f87171, #dc2626);
  color: #fff;
  font-size: 12.5px;
  font-weight: 700;
  line-height: 24px;
  text-align: center;
  border: 2.5px solid #1a2440;
  box-shadow: 0 4px 10px rgba(220, 38, 38, .5);
  transform: scale(0);
  opacity: 0;
  transition: transform .25s cubic-bezier(.34, 1.56, .64, 1), opacity .2s ease;
}
.bell__badge.is-visible { transform: scale(1); opacity: 1; }

/* クリック時のポップ演出 */
.bell__badge.is-pop { animation: badge-pop .38s cubic-bezier(.34, 1.56, .64, 1); }
@keyframes badge-pop {
  0%   { transform: scale(1); }
  40%  { transform: scale(1.35); }
  100% { transform: scale(1); }
}

.hint {
  margin: 0;
  font-size: 13px;
  color: rgba(226, 232, 240, .75);
}

.controls {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  justify-content: center;
}

.ctl {
  appearance: none;
  border: 1px solid rgba(255, 255, 255, .18);
  background: rgba(255, 255, 255, .08);
  color: #e2e8f0;
  font-size: 12.5px;
  font-weight: 600;
  padding: 9px 14px;
  border-radius: 999px;
  cursor: pointer;
  transition: background .2s ease, transform .15s ease;
}
.ctl:hover { background: rgba(255, 255, 255, .16); }
.ctl:active { transform: scale(.96); }
.ctl:focus-visible { outline: 2px solid #93c5fd; outline-offset: 2px; }
.ctl:disabled { opacity: .4; cursor: not-allowed; }

.ctl--add { border-color: rgba(96, 165, 250, .5); }
.ctl--read { border-color: rgba(248, 113, 113, .4); }

@media (prefers-reduced-motion: reduce) {
  .bell__badge, .bell__badge.is-pop { animation: none !important; transition: none !important; }
}

【JavaScript】
// 通知バッジ: +/-で件数を増減し、変化のたびにバッジをポップさせる。9件超は「9+」表記、0件は非表示。
(() => {
  const bell = document.querySelector('.bell');
  const badge = document.getElementById('bellBadge');
  const hint = document.getElementById('bellHint');
  const addBtn = document.getElementById('addBtn');
  const readBtn = document.getElementById('readBtn');
  if (!bell || !badge || !hint || !addBtn || !readBtn) return; // null安全

  const MAX_DISPLAY = 9;
  let count = 3;

  const render = () => {
    const visible = count > 0;
    badge.textContent = count > MAX_DISPLAY ? `${MAX_DISPLAY}+` : String(count);
    badge.classList.toggle('is-visible', visible);
    bell.setAttribute('aria-label', visible ? `通知、未読${count}件` : '通知、未読はありません');
    hint.textContent = visible ? `未読 ${count}件` : '未読はありません';
    readBtn.disabled = count === 0;
  };

  const pop = () => {
    badge.classList.remove('is-pop');
    void badge.offsetWidth; // リフローを挟んでアニメーションを再生成
    badge.classList.add('is-pop');
  };

  addBtn.addEventListener('click', () => {
    count += 1;
    render();
    pop();
  });

  readBtn.addEventListener('click', () => {
    if (count === 0) return;
    count -= 1;
    render();
    pop();
  });

  render();
})();

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

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