More Buttons Snippets
Like / Favorite Button — HTML CSS JS Snippet
Like / Favorite Button · Buttons · Plain HTML, CSS & JS · Live preview
What's included
Features
cubic-bezier(.2,1.6,.4,1) keyframe overshoots scale to 1.35 then settles — the bounce that makes the tap feel rewarding instead of mechanical.--tx/--ty custom properties; one keyframe translates, fades, and shrinks each one.animationend, so unlimited taps never leak nodes — critical for long feeds.bump class and reading offsetWidth forces a reflow so the count animation replays on every rapid tap.toggleFav handler drives the primary and all compact buttons; each tracks its own liked class and count, including a pre-liked example.aria-pressed between "true" and "false" so screen readers announce the like state, not just a colour change.About this UI Snippet
Like / Favorite Button — Particle Burst, Spring Heart-Pop & Animated Count

The like button is the most-tapped micro-interaction on the web. Social feeds, product galleries, comment threads, and bookmark lists all live or die on how satisfying that single tap feels. A flat heart that simply changes colour feels cheap; a heart that pops with a spring, fires a radial particle burst, and rolls its count upward feels rewarding — and that reward drives engagement. This snippet implements the full delightful version in plain HTML, CSS, and vanilla JavaScript, with no library and no SVG sprite sheet.
The effect is built from four independent layers that fire together on a single toggleFav call: the heart fill, the spring scale, the particle burst, and the count roll. Keeping them independent means you can drop any one of them without touching the others.
Spring heart-pop
When the .liked class is added, the heart's fill transitions from transparent to red and a heart-pop keyframe runs. The keyframe overshoots — scale(0.2) → 1.35 → 0.9 → 1 — using a cubic-bezier(.2,1.6,.4,1) spring curve. That overshoot is what reads as "bouncy" rather than mechanical. The animation only plays in the liked state, so un-liking is an instant, calm reset.
Radial particle burst
The burst function appends twelve .particle spans to the icon. Each particle gets a target offset from polar coordinates: the angle is evenly spaced around a circle (2π × i / 12) with a small random jitter, and the distance is randomised between 26 and 44 pixels. Those offsets are written as --tx and --ty custom properties, and the burst keyframe translates each particle to calc(-50% + var(--tx)) while fading and shrinking it. Colours cycle through a small palette. Crucially, every particle removes itself on animationend, so the DOM never accumulates dead nodes no matter how many times the user taps.
Expanding ring
A single .ring element expands from 8px to 54px while fading — the shock-wave that anchors the burst. It is the same self-cleaning animationend pattern.
Rolling, formatted count
The count is stored as text and may already be abbreviated ("1.2k"). toggleFav parses it back to a number (multiplying by 1000 when it ends in "k"), adjusts by ±1 depending on the new state, then re-formats: anything ≥ 1000 becomes (n/1000).toFixed(1) with a trailing ".0" stripped. A bump class re-triggers a short translate animation; the class is removed and a forced reflow (void offsetWidth) restarts it so rapid taps always animate.
Reusable and stateful per button
Because all state lives in classes and the count text on each button, the same toggleFav handler powers the large primary button and every compact button in the row independently — including one that starts pre-liked. Pair it with a confetti button for bigger celebrations, a product card wishlist toggle, or a social post card feed.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA large "Like 128" pill button appears above a row of three compact heart buttons (one already liked) on a dark background.
- 2Tap the big heartThe heart fills red and springs with an overshoot pop, twelve coloured particles fan out, a ring expands, and the count rolls to 129.
- 3Tap it again to un-likeThe heart instantly resets to its outline state and the count rolls back down — no burst on un-like, keeping the reverse action calm.
- 4Try the compact buttonsEach mini button keeps its own count and liked state. The "1.2k" button demonstrates the thousands formatting as it ticks up.
- 5Tap rapidlySpam-tap any button — the count animation restarts every time via a forced reflow, and particles self-clean so nothing piles up in the DOM.
- 6Adjust the burstChange the particle loop count, the distance range, or the
COLORSpalette in the JS to tune the burst to your brand.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
In toggleFav, write the state to localStorage keyed by an item id (localStorage.setItem('fav-' + id, liked)). On load, read each key and add the .liked class plus the correct aria-pressed value before the user interacts. For logged-in users, sync to your API instead so favourites follow the account across devices.
Call your API inside toggleFav with the new state, and update optimistically: change the UI immediately, then if the request fails, revert the class and count. This optimistic pattern keeps the interaction instant; the network round-trip happens invisibly in the background.
Wrap the burst and pop keyframes in @media (prefers-reduced-motion: no-preference) and provide a simple colour change as the fallback. The count still updates and aria-pressed still toggles, so the button stays fully functional without animation.
Setting --tx/--ty per element lets one shared @keyframes burst rule compute the final translate with calc(). That keeps all the timing and easing in CSS (GPU-friendly and consistent) while JavaScript only supplies the per-particle direction — cleaner than scripting each frame.
In React, hold liked and count in useState, render the heart fill from liked, and spawn particles in a small effect or a ref-based helper on click. In Vue, use refs and :class bindings with a method for the burst. In Angular, track state on the component and toggle classes with [class.liked]. The keyframes and custom-property burst CSS port unchanged.
Like / Favorite Button — Export as HTML, React, Vue, Angular & Tailwind
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Like / Favorite Button</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0f172a;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.fav-demo{text-align:center}
.fav{position:relative;display:inline-flex;align-items:center;gap:9px;background:#1e293b;border:1px solid #334155;color:#cbd5e1;font-family:inherit;font-size:15px;font-weight:700;padding:11px 18px;border-radius:999px;cursor:pointer;transition:border-color .2s,background .2s,transform .1s;-webkit-tap-highlight-color:transparent}
.fav:hover{border-color:#475569}
.fav:active{transform:scale(.95)}
.fav.liked{border-color:#f43f5e;background:rgba(244,63,94,.12);color:#fb7185}
.fav-icon{display:inline-flex;position:relative}
.fav-icon svg{fill:none;stroke:currentColor;stroke-width:2;transition:fill .2s}
.fav.liked .fav-icon svg{fill:#f43f5e;stroke:#f43f5e;animation:heart-pop .42s cubic-bezier(.2,1.6,.4,1)}
@keyframes heart-pop{0%{transform:scale(.2)}45%{transform:scale(1.35)}70%{transform:scale(.9)}100%{transform:scale(1)}}
.fav-label{transition:color .2s}
.fav.liked .fav-label{color:#fb7185}
.fav-count{font-variant-numeric:tabular-nums;min-width:1ch}
.fav-count.bump{animation:count-bump .35s ease}
@keyframes count-bump{0%{transform:translateY(0)}40%{transform:translateY(-7px);opacity:.4}100%{transform:translateY(0)}}
.fav.mini{font-size:13px;padding:8px 13px;gap:6px}
.particle{position:absolute;left:50%;top:50%;width:7px;height:7px;border-radius:50%;pointer-events:none;transform:translate(-50%,-50%);animation:burst .6s ease-out forwards}
@keyframes burst{0%{opacity:1;transform:translate(-50%,-50%) scale(1)}100%{opacity:0;transform:translate(calc(-50% + var(--tx)),calc(-50% + var(--ty))) scale(.2)}}
.ring{position:absolute;left:50%;top:50%;width:18px;height:18px;border-radius:50%;border:3px solid #f43f5e;transform:translate(-50%,-50%);pointer-events:none;animation:ring .5s ease-out forwards}
@keyframes ring{0%{opacity:.9;width:8px;height:8px}100%{opacity:0;width:54px;height:54px;border-width:1px}}
.fav-row{display:flex;gap:12px;justify-content:center;margin-top:22px}
.fav-hint{color:#64748b;font-size:13px;margin-top:24px;max-width:300px;line-height:1.5}
</style>
</head>
<body>
<div class="fav-demo">
<button class="fav" onclick="toggleFav(this)" aria-pressed="false">
<span class="fav-icon">
<svg viewBox="0 0 24 24" width="22" height="22"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.29 1.49 4.04 3 5.5l7 7Z"/></svg>
</span>
<span class="fav-label">Like</span>
<span class="fav-count" id="favCount">128</span>
</button>
<div class="fav-row">
<button class="fav mini" onclick="toggleFav(this)" aria-pressed="false">
<span class="fav-icon"><svg viewBox="0 0 24 24" width="18" height="18"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.29 1.49 4.04 3 5.5l7 7Z"/></svg></span>
<span class="fav-count">42</span>
</button>
<button class="fav mini" onclick="toggleFav(this)" aria-pressed="false">
<span class="fav-icon"><svg viewBox="0 0 24 24" width="18" height="18"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.29 1.49 4.04 3 5.5l7 7Z"/></svg></span>
<span class="fav-count">1.2k</span>
</button>
<button class="fav mini liked" onclick="toggleFav(this)" aria-pressed="true">
<span class="fav-icon"><svg viewBox="0 0 24 24" width="18" height="18"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.29 1.49 4.04 3 5.5l7 7Z"/></svg></span>
<span class="fav-count">9</span>
</button>
</div>
<p class="fav-hint">Tap a heart — particles burst, the count rolls up, and the state persists per button.</p>
</div>
<script>
var COLORS = ['#f43f5e', '#fb7185', '#fbbf24', '#f97316', '#ec4899'];
function toggleFav(btn) {
var liked = btn.classList.toggle('liked');
btn.setAttribute('aria-pressed', liked ? 'true' : 'false');
var countEl = btn.querySelector('.fav-count');
var raw = countEl.textContent.trim();
var isK = /k$/i.test(raw);
var n = parseFloat(raw) * (isK ? 1000 : 1);
n = Math.round(n) + (liked ? 1 : -1);
countEl.textContent = n >= 1000 ? (n / 1000).toFixed(1).replace(/\.0$/, '') + 'k' : n;
countEl.classList.remove('bump');
void countEl.offsetWidth;
countEl.classList.add('bump');
if (liked) burst(btn);
}
function burst(btn) {
var icon = btn.querySelector('.fav-icon');
var ring = document.createElement('span');
ring.className = 'ring';
icon.appendChild(ring);
ring.addEventListener('animationend', function () { ring.remove(); });
for (var i = 0; i < 12; i++) {
var p = document.createElement('span');
p.className = 'particle';
var angle = (Math.PI * 2 * i) / 12 + (Math.random() - 0.5);
var dist = 26 + Math.random() * 18;
p.style.setProperty('--tx', Math.cos(angle) * dist + 'px');
p.style.setProperty('--ty', Math.sin(angle) * dist + 'px');
p.style.background = COLORS[i % COLORS.length];
p.style.animationDelay = (Math.random() * 0.05) + 's';
icon.appendChild(p);
p.addEventListener('animationend', function () { this.remove(); });
}
}
</script>
</body>
</html><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Like / Favorite Button</title>
<!-- Tailwind CSS v3+ — arbitrary value classes (e.g. bg-[#0f172a]) are valid -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
@keyframes heart-pop{0%{transform:scale(.2)}45%{transform:scale(1.35)}70%{transform:scale(.9)}100%{transform:scale(1)}}
@keyframes count-bump{0%{transform:translateY(0)}40%{transform:translateY(-7px);opacity:.4}100%{transform:translateY(0)}}
@keyframes burst{0%{opacity:1;transform:translate(-50%,-50%) scale(1)}100%{opacity:0;transform:translate(calc(-50% + var(--tx)),calc(-50% + var(--ty))) scale(.2)}}
@keyframes ring{0%{opacity:.9;width:8px;height:8px}100%{opacity:0;width:54px;height:54px;border-width:1px}}
* {
box-sizing:border-box;margin:0;padding:0
}
body {
font-family:system-ui,-apple-system,sans-serif;background:#0f172a;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px
}
.fav.liked {
border-color:#f43f5e;background:rgba(244,63,94,.12);color:#fb7185
}
.fav-icon svg {
fill:none;stroke:currentColor;stroke-width:2;transition:fill .2s
}
.fav.liked .fav-icon svg {
fill:#f43f5e;stroke:#f43f5e;animation:heart-pop .42s cubic-bezier(.2,1.6,.4,1)
}
.fav.liked .fav-label {
color:#fb7185
}
.fav-count.bump {
animation:count-bump .35s ease
}
.fav.mini {
font-size:13px;padding:8px 13px;gap:6px
}
.particle {
position:absolute;left:50%;top:50%;width:7px;height:7px;border-radius:50%;pointer-events:none;transform:translate(-50%,-50%);animation:burst .6s ease-out forwards
}
.ring {
position:absolute;left:50%;top:50%;width:18px;height:18px;border-radius:50%;border:3px solid #f43f5e;transform:translate(-50%,-50%);pointer-events:none;animation:ring .5s ease-out forwards
}
</style>
</head>
<body>
<div class="text-center">
<button class="fav relative inline-flex items-center gap-[9px] bg-[#1e293b] border border-[#334155] text-[#cbd5e1] font-[inherit] text-[15px] font-bold py-[11px] px-[18px] rounded-[999px] cursor-pointer [transition:border-color_.2s,background_.2s,transform_.1s] [-webkit-tap-highlight-color:transparent] hover:border-[#475569] active:[transform:scale(.95)]" onclick="toggleFav(this)" aria-pressed="false">
<span class="fav-icon inline-flex relative">
<svg viewBox="0 0 24 24" width="22" height="22"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.29 1.49 4.04 3 5.5l7 7Z"/></svg>
</span>
<span class="fav-label [transition:color_.2s]">Like</span>
<span class="fav-count [font-variant-numeric:tabular-nums] min-w-[1ch]" id="favCount">128</span>
</button>
<div class="flex gap-3 justify-center mt-[22px]">
<button class="fav relative inline-flex items-center gap-[9px] bg-[#1e293b] border border-[#334155] text-[#cbd5e1] font-[inherit] text-[15px] font-bold py-[11px] px-[18px] rounded-[999px] cursor-pointer [transition:border-color_.2s,background_.2s,transform_.1s] [-webkit-tap-highlight-color:transparent] hover:border-[#475569] active:[transform:scale(.95)] mini" onclick="toggleFav(this)" aria-pressed="false">
<span class="fav-icon inline-flex relative"><svg viewBox="0 0 24 24" width="18" height="18"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.29 1.49 4.04 3 5.5l7 7Z"/></svg></span>
<span class="fav-count [font-variant-numeric:tabular-nums] min-w-[1ch]">42</span>
</button>
<button class="fav relative inline-flex items-center gap-[9px] bg-[#1e293b] border border-[#334155] text-[#cbd5e1] font-[inherit] text-[15px] font-bold py-[11px] px-[18px] rounded-[999px] cursor-pointer [transition:border-color_.2s,background_.2s,transform_.1s] [-webkit-tap-highlight-color:transparent] hover:border-[#475569] active:[transform:scale(.95)] mini" onclick="toggleFav(this)" aria-pressed="false">
<span class="fav-icon inline-flex relative"><svg viewBox="0 0 24 24" width="18" height="18"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.29 1.49 4.04 3 5.5l7 7Z"/></svg></span>
<span class="fav-count [font-variant-numeric:tabular-nums] min-w-[1ch]">1.2k</span>
</button>
<button class="fav relative inline-flex items-center gap-[9px] bg-[#1e293b] border border-[#334155] text-[#cbd5e1] font-[inherit] text-[15px] font-bold py-[11px] px-[18px] rounded-[999px] cursor-pointer [transition:border-color_.2s,background_.2s,transform_.1s] [-webkit-tap-highlight-color:transparent] hover:border-[#475569] active:[transform:scale(.95)] mini liked" onclick="toggleFav(this)" aria-pressed="true">
<span class="fav-icon inline-flex relative"><svg viewBox="0 0 24 24" width="18" height="18"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.29 1.49 4.04 3 5.5l7 7Z"/></svg></span>
<span class="fav-count [font-variant-numeric:tabular-nums] min-w-[1ch]">9</span>
</button>
</div>
<p class="text-[#64748b] text-[13px] mt-6 max-w-[300px] leading-normal">Tap a heart — particles burst, the count rolls up, and the state persists per button.</p>
</div>
<script>
var COLORS = ['#f43f5e', '#fb7185', '#fbbf24', '#f97316', '#ec4899'];
function toggleFav(btn) {
var liked = btn.classList.toggle('liked');
btn.setAttribute('aria-pressed', liked ? 'true' : 'false');
var countEl = btn.querySelector('.fav-count');
var raw = countEl.textContent.trim();
var isK = /k$/i.test(raw);
var n = parseFloat(raw) * (isK ? 1000 : 1);
n = Math.round(n) + (liked ? 1 : -1);
countEl.textContent = n >= 1000 ? (n / 1000).toFixed(1).replace(/\.0$/, '') + 'k' : n;
countEl.classList.remove('bump');
void countEl.offsetWidth;
countEl.classList.add('bump');
if (liked) burst(btn);
}
function burst(btn) {
var icon = btn.querySelector('.fav-icon');
var ring = document.createElement('span');
ring.className = 'ring';
icon.appendChild(ring);
ring.addEventListener('animationend', function () { ring.remove(); });
for (var i = 0; i < 12; i++) {
var p = document.createElement('span');
p.className = 'particle';
var angle = (Math.PI * 2 * i) / 12 + (Math.random() - 0.5);
var dist = 26 + Math.random() * 18;
p.style.setProperty('--tx', Math.cos(angle) * dist + 'px');
p.style.setProperty('--ty', Math.sin(angle) * dist + 'px');
p.style.background = COLORS[i % COLORS.length];
p.style.animationDelay = (Math.random() * 0.05) + 's';
icon.appendChild(p);
p.addEventListener('animationend', function () { this.remove(); });
}
}
</script>
</body>
</html>import React, { useEffect } from 'react';
// CSS — optionally move to LikeFavoriteButton.module.css
const css = `
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0f172a;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.fav-demo{text-align:center}
.fav{position:relative;display:inline-flex;align-items:center;gap:9px;background:#1e293b;border:1px solid #334155;color:#cbd5e1;font-family:inherit;font-size:15px;font-weight:700;padding:11px 18px;border-radius:999px;cursor:pointer;transition:border-color .2s,background .2s,transform .1s;-webkit-tap-highlight-color:transparent}
.fav:hover{border-color:#475569}
.fav:active{transform:scale(.95)}
.fav.liked{border-color:#f43f5e;background:rgba(244,63,94,.12);color:#fb7185}
.fav-icon{display:inline-flex;position:relative}
.fav-icon svg{fill:none;stroke:currentColor;stroke-width:2;transition:fill .2s}
.fav.liked .fav-icon svg{fill:#f43f5e;stroke:#f43f5e;animation:heart-pop .42s cubic-bezier(.2,1.6,.4,1)}
@keyframes heart-pop{0%{transform:scale(.2)}45%{transform:scale(1.35)}70%{transform:scale(.9)}100%{transform:scale(1)}}
.fav-label{transition:color .2s}
.fav.liked .fav-label{color:#fb7185}
.fav-count{font-variant-numeric:tabular-nums;min-width:1ch}
.fav-count.bump{animation:count-bump .35s ease}
@keyframes count-bump{0%{transform:translateY(0)}40%{transform:translateY(-7px);opacity:.4}100%{transform:translateY(0)}}
.fav.mini{font-size:13px;padding:8px 13px;gap:6px}
.particle{position:absolute;left:50%;top:50%;width:7px;height:7px;border-radius:50%;pointer-events:none;transform:translate(-50%,-50%);animation:burst .6s ease-out forwards}
@keyframes burst{0%{opacity:1;transform:translate(-50%,-50%) scale(1)}100%{opacity:0;transform:translate(calc(-50% + var(--tx)),calc(-50% + var(--ty))) scale(.2)}}
.ring{position:absolute;left:50%;top:50%;width:18px;height:18px;border-radius:50%;border:3px solid #f43f5e;transform:translate(-50%,-50%);pointer-events:none;animation:ring .5s ease-out forwards}
@keyframes ring{0%{opacity:.9;width:8px;height:8px}100%{opacity:0;width:54px;height:54px;border-width:1px}}
.fav-row{display:flex;gap:12px;justify-content:center;margin-top:22px}
.fav-hint{color:#64748b;font-size:13px;margin-top:24px;max-width:300px;line-height:1.5}
`;
export default function LikeFavoriteButton() {
// Auto-generated escape hatch: the original snippet's vanilla JS runs once
// after mount and queries the rendered DOM. For idiomatic React, lift this
// into state + handlers.
useEffect(() => {
const _listeners = [];
const _originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
_listeners.push({ target: this, type, listener, options });
return _originalAddEventListener.call(this, type, listener, options);
};
var COLORS = ['#f43f5e', '#fb7185', '#fbbf24', '#f97316', '#ec4899'];
function toggleFav(btn) {
var liked = btn.classList.toggle('liked');
btn.setAttribute('aria-pressed', liked ? 'true' : 'false');
var countEl = btn.querySelector('.fav-count');
var raw = countEl.textContent.trim();
var isK = /k$/i.test(raw);
var n = parseFloat(raw) * (isK ? 1000 : 1);
n = Math.round(n) + (liked ? 1 : -1);
countEl.textContent = n >= 1000 ? (n / 1000).toFixed(1).replace(/\.0$/, '') + 'k' : n;
countEl.classList.remove('bump');
void countEl.offsetWidth;
countEl.classList.add('bump');
if (liked) burst(btn);
}
function burst(btn) {
var icon = btn.querySelector('.fav-icon');
var ring = document.createElement('span');
ring.className = 'ring';
icon.appendChild(ring);
ring.addEventListener('animationend', function () { ring.remove(); });
for (var i = 0; i < 12; i++) {
var p = document.createElement('span');
p.className = 'particle';
var angle = (Math.PI * 2 * i) / 12 + (Math.random() - 0.5);
var dist = 26 + Math.random() * 18;
p.style.setProperty('--tx', Math.cos(angle) * dist + 'px');
p.style.setProperty('--ty', Math.sin(angle) * dist + 'px');
p.style.background = COLORS[i % COLORS.length];
p.style.animationDelay = (Math.random() * 0.05) + 's';
icon.appendChild(p);
p.addEventListener('animationend', function () { this.remove(); });
}
}
// Cleanup: restore addEventListener and remove all listeners
return () => {
EventTarget.prototype.addEventListener = _originalAddEventListener;
for (const { target, type, listener, options } of _listeners) {
target.removeEventListener(type, listener, options);
}
};
// Expose handlers used by inline JSX events to the global scope
if (typeof toggleFav === 'function') window.toggleFav = toggleFav;
}, []);
return (
<>
<style>{css}</style>
<div className="fav-demo">
<button className="fav" onClick={(event) => { toggleFav(event.currentTarget) }} aria-pressed="false">
<span className="fav-icon">
<svg viewBox="0 0 24 24" width="22" height="22"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.29 1.49 4.04 3 5.5l7 7Z"/></svg>
</span>
<span className="fav-label">Like</span>
<span className="fav-count" id="favCount">128</span>
</button>
<div className="fav-row">
<button className="fav mini" onClick={(event) => { toggleFav(event.currentTarget) }} aria-pressed="false">
<span className="fav-icon"><svg viewBox="0 0 24 24" width="18" height="18"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.29 1.49 4.04 3 5.5l7 7Z"/></svg></span>
<span className="fav-count">42</span>
</button>
<button className="fav mini" onClick={(event) => { toggleFav(event.currentTarget) }} aria-pressed="false">
<span className="fav-icon"><svg viewBox="0 0 24 24" width="18" height="18"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.29 1.49 4.04 3 5.5l7 7Z"/></svg></span>
<span className="fav-count">1.2k</span>
</button>
<button className="fav mini liked" onClick={(event) => { toggleFav(event.currentTarget) }} aria-pressed="true">
<span className="fav-icon"><svg viewBox="0 0 24 24" width="18" height="18"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.29 1.49 4.04 3 5.5l7 7Z"/></svg></span>
<span className="fav-count">9</span>
</button>
</div>
<p className="fav-hint">Tap a heart — particles burst, the count rolls up, and the state persists per button.</p>
</div>
</>
);
}import React, { useEffect } from 'react';
// Requires Tailwind CSS v3+ — https://tailwindcss.com/docs/installation
// Arbitrary value classes (e.g. bg-[#0f172a]) are valid Tailwind v3+
export default function LikeFavoriteButton() {
// Auto-generated escape hatch: the original snippet's vanilla JS runs once
// after mount and queries the rendered DOM. For idiomatic React, lift this
// into state + handlers.
useEffect(() => {
const _listeners = [];
const _originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
_listeners.push({ target: this, type, listener, options });
return _originalAddEventListener.call(this, type, listener, options);
};
var COLORS = ['#f43f5e', '#fb7185', '#fbbf24', '#f97316', '#ec4899'];
function toggleFav(btn) {
var liked = btn.classList.toggle('liked');
btn.setAttribute('aria-pressed', liked ? 'true' : 'false');
var countEl = btn.querySelector('.fav-count');
var raw = countEl.textContent.trim();
var isK = /k$/i.test(raw);
var n = parseFloat(raw) * (isK ? 1000 : 1);
n = Math.round(n) + (liked ? 1 : -1);
countEl.textContent = n >= 1000 ? (n / 1000).toFixed(1).replace(/\.0$/, '') + 'k' : n;
countEl.classList.remove('bump');
void countEl.offsetWidth;
countEl.classList.add('bump');
if (liked) burst(btn);
}
function burst(btn) {
var icon = btn.querySelector('.fav-icon');
var ring = document.createElement('span');
ring.className = 'ring';
icon.appendChild(ring);
ring.addEventListener('animationend', function () { ring.remove(); });
for (var i = 0; i < 12; i++) {
var p = document.createElement('span');
p.className = 'particle';
var angle = (Math.PI * 2 * i) / 12 + (Math.random() - 0.5);
var dist = 26 + Math.random() * 18;
p.style.setProperty('--tx', Math.cos(angle) * dist + 'px');
p.style.setProperty('--ty', Math.sin(angle) * dist + 'px');
p.style.background = COLORS[i % COLORS.length];
p.style.animationDelay = (Math.random() * 0.05) + 's';
icon.appendChild(p);
p.addEventListener('animationend', function () { this.remove(); });
}
}
// Cleanup: restore addEventListener and remove all listeners
return () => {
EventTarget.prototype.addEventListener = _originalAddEventListener;
for (const { target, type, listener, options } of _listeners) {
target.removeEventListener(type, listener, options);
}
};
// Expose handlers used by inline JSX events to the global scope
if (typeof toggleFav === 'function') window.toggleFav = toggleFav;
}, []);
return (
<>
<style>{`
@keyframes heart-pop{0%{transform:scale(.2)}45%{transform:scale(1.35)}70%{transform:scale(.9)}100%{transform:scale(1)}}
@keyframes count-bump{0%{transform:translateY(0)}40%{transform:translateY(-7px);opacity:.4}100%{transform:translateY(0)}}
@keyframes burst{0%{opacity:1;transform:translate(-50%,-50%) scale(1)}100%{opacity:0;transform:translate(calc(-50% + var(--tx)),calc(-50% + var(--ty))) scale(.2)}}
@keyframes ring{0%{opacity:.9;width:8px;height:8px}100%{opacity:0;width:54px;height:54px;border-width:1px}}
* {
box-sizing:border-box;margin:0;padding:0
}
body {
font-family:system-ui,-apple-system,sans-serif;background:#0f172a;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px
}
.fav.liked {
border-color:#f43f5e;background:rgba(244,63,94,.12);color:#fb7185
}
.fav-icon svg {
fill:none;stroke:currentColor;stroke-width:2;transition:fill .2s
}
.fav.liked .fav-icon svg {
fill:#f43f5e;stroke:#f43f5e;animation:heart-pop .42s cubic-bezier(.2,1.6,.4,1)
}
.fav.liked .fav-label {
color:#fb7185
}
.fav-count.bump {
animation:count-bump .35s ease
}
.fav.mini {
font-size:13px;padding:8px 13px;gap:6px
}
.particle {
position:absolute;left:50%;top:50%;width:7px;height:7px;border-radius:50%;pointer-events:none;transform:translate(-50%,-50%);animation:burst .6s ease-out forwards
}
.ring {
position:absolute;left:50%;top:50%;width:18px;height:18px;border-radius:50%;border:3px solid #f43f5e;transform:translate(-50%,-50%);pointer-events:none;animation:ring .5s ease-out forwards
}
`}</style>
<div className="text-center">
<button className="fav relative inline-flex items-center gap-[9px] bg-[#1e293b] border border-[#334155] text-[#cbd5e1] font-[inherit] text-[15px] font-bold py-[11px] px-[18px] rounded-[999px] cursor-pointer [transition:border-color_.2s,background_.2s,transform_.1s] [-webkit-tap-highlight-color:transparent] hover:border-[#475569] active:[transform:scale(.95)]" onClick={(event) => { toggleFav(event.currentTarget) }} aria-pressed="false">
<span className="fav-icon inline-flex relative">
<svg viewBox="0 0 24 24" width="22" height="22"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.29 1.49 4.04 3 5.5l7 7Z"/></svg>
</span>
<span className="fav-label [transition:color_.2s]">Like</span>
<span className="fav-count [font-variant-numeric:tabular-nums] min-w-[1ch]" id="favCount">128</span>
</button>
<div className="flex gap-3 justify-center mt-[22px]">
<button className="fav relative inline-flex items-center gap-[9px] bg-[#1e293b] border border-[#334155] text-[#cbd5e1] font-[inherit] text-[15px] font-bold py-[11px] px-[18px] rounded-[999px] cursor-pointer [transition:border-color_.2s,background_.2s,transform_.1s] [-webkit-tap-highlight-color:transparent] hover:border-[#475569] active:[transform:scale(.95)] mini" onClick={(event) => { toggleFav(event.currentTarget) }} aria-pressed="false">
<span className="fav-icon inline-flex relative"><svg viewBox="0 0 24 24" width="18" height="18"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.29 1.49 4.04 3 5.5l7 7Z"/></svg></span>
<span className="fav-count [font-variant-numeric:tabular-nums] min-w-[1ch]">42</span>
</button>
<button className="fav relative inline-flex items-center gap-[9px] bg-[#1e293b] border border-[#334155] text-[#cbd5e1] font-[inherit] text-[15px] font-bold py-[11px] px-[18px] rounded-[999px] cursor-pointer [transition:border-color_.2s,background_.2s,transform_.1s] [-webkit-tap-highlight-color:transparent] hover:border-[#475569] active:[transform:scale(.95)] mini" onClick={(event) => { toggleFav(event.currentTarget) }} aria-pressed="false">
<span className="fav-icon inline-flex relative"><svg viewBox="0 0 24 24" width="18" height="18"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.29 1.49 4.04 3 5.5l7 7Z"/></svg></span>
<span className="fav-count [font-variant-numeric:tabular-nums] min-w-[1ch]">1.2k</span>
</button>
<button className="fav relative inline-flex items-center gap-[9px] bg-[#1e293b] border border-[#334155] text-[#cbd5e1] font-[inherit] text-[15px] font-bold py-[11px] px-[18px] rounded-[999px] cursor-pointer [transition:border-color_.2s,background_.2s,transform_.1s] [-webkit-tap-highlight-color:transparent] hover:border-[#475569] active:[transform:scale(.95)] mini liked" onClick={(event) => { toggleFav(event.currentTarget) }} aria-pressed="true">
<span className="fav-icon inline-flex relative"><svg viewBox="0 0 24 24" width="18" height="18"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.29 1.49 4.04 3 5.5l7 7Z"/></svg></span>
<span className="fav-count [font-variant-numeric:tabular-nums] min-w-[1ch]">9</span>
</button>
</div>
<p className="text-[#64748b] text-[13px] mt-6 max-w-[300px] leading-normal">Tap a heart — particles burst, the count rolls up, and the state persists per button.</p>
</div>
</>
);
}<template>
<div class="fav-demo">
<button class="fav" @click="toggleFav($event.currentTarget)" aria-pressed="false">
<span class="fav-icon">
<svg viewBox="0 0 24 24" width="22" height="22"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.29 1.49 4.04 3 5.5l7 7Z"/></svg>
</span>
<span class="fav-label">Like</span>
<span class="fav-count" id="favCount">128</span>
</button>
<div class="fav-row">
<button class="fav mini" @click="toggleFav($event.currentTarget)" aria-pressed="false">
<span class="fav-icon"><svg viewBox="0 0 24 24" width="18" height="18"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.29 1.49 4.04 3 5.5l7 7Z"/></svg></span>
<span class="fav-count">42</span>
</button>
<button class="fav mini" @click="toggleFav($event.currentTarget)" aria-pressed="false">
<span class="fav-icon"><svg viewBox="0 0 24 24" width="18" height="18"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.29 1.49 4.04 3 5.5l7 7Z"/></svg></span>
<span class="fav-count">1.2k</span>
</button>
<button class="fav mini liked" @click="toggleFav($event.currentTarget)" aria-pressed="true">
<span class="fav-icon"><svg viewBox="0 0 24 24" width="18" height="18"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.29 1.49 4.04 3 5.5l7 7Z"/></svg></span>
<span class="fav-count">9</span>
</button>
</div>
<p class="fav-hint">Tap a heart — particles burst, the count rolls up, and the state persists per button.</p>
</div>
</template>
<script setup>
import { onMounted } from 'vue';
var COLORS = ['#f43f5e', '#fb7185', '#fbbf24', '#f97316', '#ec4899'];
function toggleFav(btn) {
var liked = btn.classList.toggle('liked');
btn.setAttribute('aria-pressed', liked ? 'true' : 'false');
var countEl = btn.querySelector('.fav-count');
var raw = countEl.textContent.trim();
var isK = /k$/i.test(raw);
var n = parseFloat(raw) * (isK ? 1000 : 1);
n = Math.round(n) + (liked ? 1 : -1);
countEl.textContent = n >= 1000 ? (n / 1000).toFixed(1).replace(/\.0$/, '') + 'k' : n;
countEl.classList.remove('bump');
void countEl.offsetWidth;
countEl.classList.add('bump');
if (liked) burst(btn);
}
function burst(btn) {
var icon = btn.querySelector('.fav-icon');
var ring = document.createElement('span');
ring.className = 'ring';
icon.appendChild(ring);
ring.addEventListener('animationend', function () { ring.remove(); });
for (var i = 0; i < 12; i++) {
var p = document.createElement('span');
p.className = 'particle';
var angle = (Math.PI * 2 * i) / 12 + (Math.random() - 0.5);
var dist = 26 + Math.random() * 18;
p.style.setProperty('--tx', Math.cos(angle) * dist + 'px');
p.style.setProperty('--ty', Math.sin(angle) * dist + 'px');
p.style.background = COLORS[i % COLORS.length];
p.style.animationDelay = (Math.random() * 0.05) + 's';
icon.appendChild(p);
p.addEventListener('animationend', function () { this.remove(); });
}
}
</script>
<style scoped>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0f172a;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.fav-demo{text-align:center}
.fav{position:relative;display:inline-flex;align-items:center;gap:9px;background:#1e293b;border:1px solid #334155;color:#cbd5e1;font-family:inherit;font-size:15px;font-weight:700;padding:11px 18px;border-radius:999px;cursor:pointer;transition:border-color .2s,background .2s,transform .1s;-webkit-tap-highlight-color:transparent}
.fav:hover{border-color:#475569}
.fav:active{transform:scale(.95)}
.fav.liked{border-color:#f43f5e;background:rgba(244,63,94,.12);color:#fb7185}
.fav-icon{display:inline-flex;position:relative}
.fav-icon svg{fill:none;stroke:currentColor;stroke-width:2;transition:fill .2s}
.fav.liked .fav-icon svg{fill:#f43f5e;stroke:#f43f5e;animation:heart-pop .42s cubic-bezier(.2,1.6,.4,1)}
@keyframes heart-pop{0%{transform:scale(.2)}45%{transform:scale(1.35)}70%{transform:scale(.9)}100%{transform:scale(1)}}
.fav-label{transition:color .2s}
.fav.liked .fav-label{color:#fb7185}
.fav-count{font-variant-numeric:tabular-nums;min-width:1ch}
.fav-count.bump{animation:count-bump .35s ease}
@keyframes count-bump{0%{transform:translateY(0)}40%{transform:translateY(-7px);opacity:.4}100%{transform:translateY(0)}}
.fav.mini{font-size:13px;padding:8px 13px;gap:6px}
.particle{position:absolute;left:50%;top:50%;width:7px;height:7px;border-radius:50%;pointer-events:none;transform:translate(-50%,-50%);animation:burst .6s ease-out forwards}
@keyframes burst{0%{opacity:1;transform:translate(-50%,-50%) scale(1)}100%{opacity:0;transform:translate(calc(-50% + var(--tx)),calc(-50% + var(--ty))) scale(.2)}}
.ring{position:absolute;left:50%;top:50%;width:18px;height:18px;border-radius:50%;border:3px solid #f43f5e;transform:translate(-50%,-50%);pointer-events:none;animation:ring .5s ease-out forwards}
@keyframes ring{0%{opacity:.9;width:8px;height:8px}100%{opacity:0;width:54px;height:54px;border-width:1px}}
.fav-row{display:flex;gap:12px;justify-content:center;margin-top:22px}
.fav-hint{color:#64748b;font-size:13px;margin-top:24px;max-width:300px;line-height:1.5}
</style>// @ts-nocheck
// Note: vanilla JS DOM manipulation is preserved as-is inside ngAfterViewInit().
// For idiomatic Angular, replace document.getElementById() with @ViewChild() refs
// and move state into component properties with two-way binding.
import { Component, AfterViewInit, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-like-favorite-button',
standalone: true,
imports: [CommonModule],
encapsulation: ViewEncapsulation.None,
template: `
<div class="fav-demo">
<button class="fav" (click)="toggleFav($event.currentTarget)" aria-pressed="false">
<span class="fav-icon">
<svg viewBox="0 0 24 24" width="22" height="22"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.29 1.49 4.04 3 5.5l7 7Z"/></svg>
</span>
<span class="fav-label">Like</span>
<span class="fav-count" id="favCount">128</span>
</button>
<div class="fav-row">
<button class="fav mini" (click)="toggleFav($event.currentTarget)" aria-pressed="false">
<span class="fav-icon"><svg viewBox="0 0 24 24" width="18" height="18"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.29 1.49 4.04 3 5.5l7 7Z"/></svg></span>
<span class="fav-count">42</span>
</button>
<button class="fav mini" (click)="toggleFav($event.currentTarget)" aria-pressed="false">
<span class="fav-icon"><svg viewBox="0 0 24 24" width="18" height="18"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.29 1.49 4.04 3 5.5l7 7Z"/></svg></span>
<span class="fav-count">1.2k</span>
</button>
<button class="fav mini liked" (click)="toggleFav($event.currentTarget)" aria-pressed="true">
<span class="fav-icon"><svg viewBox="0 0 24 24" width="18" height="18"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.29 1.49 4.04 3 5.5l7 7Z"/></svg></span>
<span class="fav-count">9</span>
</button>
</div>
<p class="fav-hint">Tap a heart — particles burst, the count rolls up, and the state persists per button.</p>
</div>
`,
styles: [`
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0f172a;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.fav-demo{text-align:center}
.fav{position:relative;display:inline-flex;align-items:center;gap:9px;background:#1e293b;border:1px solid #334155;color:#cbd5e1;font-family:inherit;font-size:15px;font-weight:700;padding:11px 18px;border-radius:999px;cursor:pointer;transition:border-color .2s,background .2s,transform .1s;-webkit-tap-highlight-color:transparent}
.fav:hover{border-color:#475569}
.fav:active{transform:scale(.95)}
.fav.liked{border-color:#f43f5e;background:rgba(244,63,94,.12);color:#fb7185}
.fav-icon{display:inline-flex;position:relative}
.fav-icon svg{fill:none;stroke:currentColor;stroke-width:2;transition:fill .2s}
.fav.liked .fav-icon svg{fill:#f43f5e;stroke:#f43f5e;animation:heart-pop .42s cubic-bezier(.2,1.6,.4,1)}
@keyframes heart-pop{0%{transform:scale(.2)}45%{transform:scale(1.35)}70%{transform:scale(.9)}100%{transform:scale(1)}}
.fav-label{transition:color .2s}
.fav.liked .fav-label{color:#fb7185}
.fav-count{font-variant-numeric:tabular-nums;min-width:1ch}
.fav-count.bump{animation:count-bump .35s ease}
@keyframes count-bump{0%{transform:translateY(0)}40%{transform:translateY(-7px);opacity:.4}100%{transform:translateY(0)}}
.fav.mini{font-size:13px;padding:8px 13px;gap:6px}
.particle{position:absolute;left:50%;top:50%;width:7px;height:7px;border-radius:50%;pointer-events:none;transform:translate(-50%,-50%);animation:burst .6s ease-out forwards}
@keyframes burst{0%{opacity:1;transform:translate(-50%,-50%) scale(1)}100%{opacity:0;transform:translate(calc(-50% + var(--tx)),calc(-50% + var(--ty))) scale(.2)}}
.ring{position:absolute;left:50%;top:50%;width:18px;height:18px;border-radius:50%;border:3px solid #f43f5e;transform:translate(-50%,-50%);pointer-events:none;animation:ring .5s ease-out forwards}
@keyframes ring{0%{opacity:.9;width:8px;height:8px}100%{opacity:0;width:54px;height:54px;border-width:1px}}
.fav-row{display:flex;gap:12px;justify-content:center;margin-top:22px}
.fav-hint{color:#64748b;font-size:13px;margin-top:24px;max-width:300px;line-height:1.5}
`]
})
export class LikeFavoriteButtonComponent implements AfterViewInit {
ngAfterViewInit(): void {
var COLORS = ['#f43f5e', '#fb7185', '#fbbf24', '#f97316', '#ec4899'];
function toggleFav(btn) {
var liked = btn.classList.toggle('liked');
btn.setAttribute('aria-pressed', liked ? 'true' : 'false');
var countEl = btn.querySelector('.fav-count');
var raw = countEl.textContent.trim();
var isK = /k$/i.test(raw);
var n = parseFloat(raw) * (isK ? 1000 : 1);
n = Math.round(n) + (liked ? 1 : -1);
countEl.textContent = n >= 1000 ? (n / 1000).toFixed(1).replace(/\.0$/, '') + 'k' : n;
countEl.classList.remove('bump');
void countEl.offsetWidth;
countEl.classList.add('bump');
if (liked) burst(btn);
}
function burst(btn) {
var icon = btn.querySelector('.fav-icon');
var ring = document.createElement('span');
ring.className = 'ring';
icon.appendChild(ring);
ring.addEventListener('animationend', function () { ring.remove(); });
for (var i = 0; i < 12; i++) {
var p = document.createElement('span');
p.className = 'particle';
var angle = (Math.PI * 2 * i) / 12 + (Math.random() - 0.5);
var dist = 26 + Math.random() * 18;
p.style.setProperty('--tx', Math.cos(angle) * dist + 'px');
p.style.setProperty('--ty', Math.sin(angle) * dist + 'px');
p.style.background = COLORS[i % COLORS.length];
p.style.animationDelay = (Math.random() * 0.05) + 's';
icon.appendChild(p);
p.addEventListener('animationend', function () { this.remove(); });
}
}
// Bind inline-handler functions to the component so the template can call them
Object.assign(this, { toggleFav, burst });
}
}