はじけるハート
クリックのたびにハートが弾性スケールで弾み、ハートや粒が放射状に飛び散ってフェードするいいねボタン。連打しても崩れません。
ライブデモ
コード
HTML
<!-- はじけるハート: クリックで弾性スケール+放射状の粒子が飛び散るいいねボタン -->
<div class="stage">
<button class="heart-burst" type="button" aria-label="いいね">
<svg class="heart-burst__icon" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 21s-7.5-4.6-10-9.2C.3 8.6 1.8 5 5.2 5c2 0 3.3 1.1 4 2.2C9.9 6.1 11.2 5 13.2 5c3.4 0 4.9 3.6 3.2 6.8C19.5 16.4 12 21 12 21z"/>
</svg>
<span class="heart-burst__count">128</span>
</button>
<p class="hint">クリックするたびにカウントアップ</p>
</div>
CSS
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
overflow: hidden;
display: grid;
place-items: center;
font-family: "Segoe UI", system-ui, sans-serif;
background: radial-gradient(circle at 50% 32%, #201626 0%, #0d0e1a 72%);
color: #f4f5fb;
}
.stage { display: grid; place-items: center; gap: 16px; }
.heart-burst {
position: relative;
display: inline-flex;
align-items: center;
gap: 12px;
padding: 14px 26px;
border: none;
border-radius: 999px;
cursor: pointer;
background: rgba(255, 255, 255, .05);
box-shadow: 0 10px 26px rgba(0, 0, 0, .4);
transition: transform .12s ease;
-webkit-tap-highlight-color: transparent;
animation: heart-idle 3.2s ease-in-out infinite;
}
.heart-burst:active { transform: scale(.94); }
.heart-burst:focus-visible { outline: 2px solid #fb7185; outline-offset: 4px; }
@keyframes heart-idle {
0%, 100% { box-shadow: 0 10px 26px rgba(0, 0, 0, .4), 0 0 0 rgba(251, 113, 133, 0); }
50% { box-shadow: 0 10px 26px rgba(0, 0, 0, .4), 0 0 14px rgba(251, 113, 133, .22); }
}
.heart-burst__icon {
width: 28px;
height: 28px;
fill: #565b74;
transform-origin: center;
transition: fill .2s ease;
}
.heart-burst.is-pop .heart-burst__icon {
fill: #fb7185;
animation: heart-pop .5s cubic-bezier(.34, 1.56, .64, 1);
}
@keyframes heart-pop {
0% { transform: scale(.8); }
55% { transform: scale(1.3); }
100% { transform: scale(1); }
}
.heart-burst__count {
font-size: 17px;
font-weight: 700;
color: #f4f5fb;
font-variant-numeric: tabular-nums;
min-width: 2.6ch;
}
/* 放射する粒子(♥や•)。JSが都度生成し、アニメ終了で自身を除去する */
.spark {
position: absolute;
top: 50%;
left: 27px;
width: 7px;
height: 7px;
pointer-events: none;
transform: translate(-50%, -50%);
font-size: 12px;
line-height: 1;
display: grid;
place-items: center;
animation: spark-fly .65s ease-out forwards;
}
@keyframes spark-fly {
0% { opacity: 1; transform: translate(-50%, -50%) translate(0, 0) rotate(0deg) scale(1); }
100% { opacity: 0; transform: translate(-50%, -50%) translate(var(--dx), var(--dy)) rotate(var(--rot)) scale(.3); }
}
.hint { margin: 0; font-size: 13px; color: rgba(244, 245, 251, .5); }
@media (prefers-reduced-motion: reduce) {
.heart-burst { animation: none; }
.heart-burst.is-pop .heart-burst__icon { animation: none; }
.spark { animation-duration: .01ms; }
}
JavaScript
// はじけるハート: クリックのたびにカウントアップし、弾性スケール+放射状の粒子で反応する
(() => {
const btn = document.querySelector('.heart-burst');
const countEl = document.querySelector('.heart-burst__count');
if (!btn || !countEl) return; // null安全
const base = parseInt(countEl.textContent, 10) || 0;
let count = base;
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const COLORS = ['#fb7185', '#fbbf24', '#f9a8d4'];
const GLYPHS = ['♥', '♥', '•'];
const burst = () => {
if (reduce) return;
const N = 9;
for (let i = 0; i < N; i++) {
const s = document.createElement('span');
s.className = 'spark';
s.textContent = GLYPHS[Math.floor(Math.random() * GLYPHS.length)];
const angle = (Math.PI * 2 * i) / N + (Math.random() - 0.5) * 0.5;
const dist = 30 + Math.random() * 26;
s.style.setProperty('--dx', `${Math.cos(angle) * dist}px`);
s.style.setProperty('--dy', `${Math.sin(angle) * dist}px`);
s.style.setProperty('--rot', `${(Math.random() - 0.5) * 200}deg`);
s.style.color = COLORS[i % COLORS.length];
s.addEventListener('animationend', () => s.remove());
btn.appendChild(s);
}
};
btn.addEventListener('click', () => {
count += 1;
countEl.textContent = String(count);
// 連打しても弾む動きが破綻しないよう、一度クラスを外してreflowしてから再付与する
btn.classList.remove('is-pop');
void btn.offsetWidth; // reflow
btn.classList.add('is-pop');
burst();
});
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「はじけるハート」の効果を追加してください。
# 追加してほしい効果
はじけるハート(遊べるUI)
クリックのたびにハートが弾性スケールで弾み、ハートや粒が放射状に飛び散ってフェードするいいねボタン。連打しても崩れません。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- はじけるハート: クリックで弾性スケール+放射状の粒子が飛び散るいいねボタン -->
<div class="stage">
<button class="heart-burst" type="button" aria-label="いいね">
<svg class="heart-burst__icon" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 21s-7.5-4.6-10-9.2C.3 8.6 1.8 5 5.2 5c2 0 3.3 1.1 4 2.2C9.9 6.1 11.2 5 13.2 5c3.4 0 4.9 3.6 3.2 6.8C19.5 16.4 12 21 12 21z"/>
</svg>
<span class="heart-burst__count">128</span>
</button>
<p class="hint">クリックするたびにカウントアップ</p>
</div>
【CSS】
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
overflow: hidden;
display: grid;
place-items: center;
font-family: "Segoe UI", system-ui, sans-serif;
background: radial-gradient(circle at 50% 32%, #201626 0%, #0d0e1a 72%);
color: #f4f5fb;
}
.stage { display: grid; place-items: center; gap: 16px; }
.heart-burst {
position: relative;
display: inline-flex;
align-items: center;
gap: 12px;
padding: 14px 26px;
border: none;
border-radius: 999px;
cursor: pointer;
background: rgba(255, 255, 255, .05);
box-shadow: 0 10px 26px rgba(0, 0, 0, .4);
transition: transform .12s ease;
-webkit-tap-highlight-color: transparent;
animation: heart-idle 3.2s ease-in-out infinite;
}
.heart-burst:active { transform: scale(.94); }
.heart-burst:focus-visible { outline: 2px solid #fb7185; outline-offset: 4px; }
@keyframes heart-idle {
0%, 100% { box-shadow: 0 10px 26px rgba(0, 0, 0, .4), 0 0 0 rgba(251, 113, 133, 0); }
50% { box-shadow: 0 10px 26px rgba(0, 0, 0, .4), 0 0 14px rgba(251, 113, 133, .22); }
}
.heart-burst__icon {
width: 28px;
height: 28px;
fill: #565b74;
transform-origin: center;
transition: fill .2s ease;
}
.heart-burst.is-pop .heart-burst__icon {
fill: #fb7185;
animation: heart-pop .5s cubic-bezier(.34, 1.56, .64, 1);
}
@keyframes heart-pop {
0% { transform: scale(.8); }
55% { transform: scale(1.3); }
100% { transform: scale(1); }
}
.heart-burst__count {
font-size: 17px;
font-weight: 700;
color: #f4f5fb;
font-variant-numeric: tabular-nums;
min-width: 2.6ch;
}
/* 放射する粒子(♥や•)。JSが都度生成し、アニメ終了で自身を除去する */
.spark {
position: absolute;
top: 50%;
left: 27px;
width: 7px;
height: 7px;
pointer-events: none;
transform: translate(-50%, -50%);
font-size: 12px;
line-height: 1;
display: grid;
place-items: center;
animation: spark-fly .65s ease-out forwards;
}
@keyframes spark-fly {
0% { opacity: 1; transform: translate(-50%, -50%) translate(0, 0) rotate(0deg) scale(1); }
100% { opacity: 0; transform: translate(-50%, -50%) translate(var(--dx), var(--dy)) rotate(var(--rot)) scale(.3); }
}
.hint { margin: 0; font-size: 13px; color: rgba(244, 245, 251, .5); }
@media (prefers-reduced-motion: reduce) {
.heart-burst { animation: none; }
.heart-burst.is-pop .heart-burst__icon { animation: none; }
.spark { animation-duration: .01ms; }
}
【JavaScript】
// はじけるハート: クリックのたびにカウントアップし、弾性スケール+放射状の粒子で反応する
(() => {
const btn = document.querySelector('.heart-burst');
const countEl = document.querySelector('.heart-burst__count');
if (!btn || !countEl) return; // null安全
const base = parseInt(countEl.textContent, 10) || 0;
let count = base;
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const COLORS = ['#fb7185', '#fbbf24', '#f9a8d4'];
const GLYPHS = ['♥', '♥', '•'];
const burst = () => {
if (reduce) return;
const N = 9;
for (let i = 0; i < N; i++) {
const s = document.createElement('span');
s.className = 'spark';
s.textContent = GLYPHS[Math.floor(Math.random() * GLYPHS.length)];
const angle = (Math.PI * 2 * i) / N + (Math.random() - 0.5) * 0.5;
const dist = 30 + Math.random() * 26;
s.style.setProperty('--dx', `${Math.cos(angle) * dist}px`);
s.style.setProperty('--dy', `${Math.sin(angle) * dist}px`);
s.style.setProperty('--rot', `${(Math.random() - 0.5) * 200}deg`);
s.style.color = COLORS[i % COLORS.length];
s.addEventListener('animationend', () => s.remove());
btn.appendChild(s);
}
};
btn.addEventListener('click', () => {
count += 1;
countEl.textContent = String(count);
// 連打しても弾む動きが破綻しないよう、一度クラスを外してreflowしてから再付与する
btn.classList.remove('is-pop');
void btn.offsetWidth; // reflow
btn.classList.add('is-pop');
burst();
});
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。