Like / Favorite Button — HTML CSS JS Snippet

Like / Favorite Button · Buttons · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Spring heart-pop
A 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.
Radial particle burst
Twelve particles get polar-coordinate targets written as --tx/--ty custom properties; one keyframe translates, fades, and shrinks each one.
Self-cleaning DOM
Every particle and the ring remove themselves on animationend, so unlimited taps never leak nodes — critical for long feeds.
Expanding shock-wave ring
A single element grows from 8px to 54px while thinning its border and fading, anchoring the burst at the heart's centre.
Rolling abbreviated count
Parses "1.2k"-style text back to a number, adjusts by ±1, and re-formats values ≥1000 with a stripped trailing ".0".
Reflow-restarted count bump
Removing the bump class and reading offsetWidth forces a reflow so the count animation replays on every rapid tap.
Per-button independent state
One toggleFav handler drives the primary and all compact buttons; each tracks its own liked class and count, including a pre-liked example.
Accessible toggle semantics
Each button updates 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

Screenshot of the Like / Favorite Button snippet rendered live

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

  1. 1
    Paste 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.
  2. 2
    Tap 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.
  3. 3
    Tap 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.
  4. 4
    Try the compact buttonsEach mini button keeps its own count and liked state. The "1.2k" button demonstrates the thousands formatting as it ticks up.
  5. 5
    Tap 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.
  6. 6
    Adjust the burstChange the particle loop count, the distance range, or the COLORS palette in the JS to tune the burst to your brand.

Real-world uses

Common Use Cases

Social feed reactions
The classic use — like posts, photos, and comments. Drop it into a social post card or an activity feed row.
Product wishlist toggle
Save items to favourites from a catalogue. Wire it into a product card corner so shoppers can heart products without leaving the grid.
Article and content bookmarking
Let readers favourite articles or docs. Swap the heart path for a bookmark glyph and reuse the burst and count logic unchanged.
Comment and review upvotes
Use it as an upvote control in a comment thread — the count roll and burst make voting feel responsive.
Gamified achievement taps
Celebrate milestones with the burst. For bigger moments, escalate to a confetti button on the final tap.
Music and media favourites
Heart tracks or videos in a player UI; the compact variant fits neatly into a music player control bar.

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

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>
  <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>
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>
  <!-- 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>
React
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>
    </>
  );
}
React + Tailwind
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>
    </>
  );
}
Vue
<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>
Angular
// @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 });
  }
}