パルス通知バッジ
アイコン右上のカウントを波紋とポップで強調する通知バッジ。更新時のアテンション喚起に使えます。
ライブデモ
使用例(お題: アイドルグループ 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で提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。