24 CSS Animated Cards08 / 24

Light JSMIT licensed

Like/Save Burst Micro-Interaction

The like that feels good to press: an overshooting heart, twelve particles thrown out on trigonometric angles, an expanding ring, and a count that rolls over — all CSS keyframes fired by a single class toggle. The particles are pure CSS (no canvas, no library), positioned with a rotate-then-translate trick so twelve elements need one keyframe between them.

Published

Live Demo
Try it

The code

<section class="ac-08" aria-label="Like and save burst micro-interaction demo">
  <div class="ac-08__stage">
    <header class="ac-08__head">
      <p class="ac-08__eyebrow">Micro-interaction</p>
      <h2 class="ac-08__title">A like worth pressing twice</h2>
    </header>
    <ul class="ac-08__grid" role="list">
      <li class="ac-08__card">
        <span class="ac-08__media"><img src="https://images.unsplash.com/photo-1517841905240-472988babdf9?q=80&w=700&auto=format&fit=crop" alt="Portrait of a woman in warm light" loading="lazy" decoding="async" width="700" height="560"></span>
        <div class="ac-08__body">
          <div class="ac-08__meta"><h3 class="ac-08__name">Golden hour, no filter</h3><p class="ac-08__by">@rennadavis</p></div>
          <div class="ac-08__acts">
            <button class="ac-08__like" type="button" data-like aria-pressed="false">
              <span class="ac-08__ring" aria-hidden="true"></span>
              <span class="ac-08__burst" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i></span>
              <svg class="ac-08__heart" viewBox="0 0 24 24" width="21" height="21" aria-hidden="true"><path d="M12 20.4l-1.5-1.36C5.4 14.4 2.4 11.7 2.4 8.4 2.4 5.9 4.4 4 6.9 4c1.5 0 2.9.7 3.8 1.8l1.3 1.5 1.3-1.5C14.2 4.7 15.6 4 17.1 4c2.5 0 4.5 1.9 4.5 4.4 0 3.3-3 6-8.1 10.66L12 20.4z"/></svg>
              <b class="ac-08__count" data-count>1,204</b>
              <span class="ac-08__sr" data-likesr>Like this post</span>
            </button>
            <button class="ac-08__save" type="button" data-save aria-pressed="false">
              <svg viewBox="0 0 24 24" width="19" height="19" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round" aria-hidden="true"><path d="M6 4h12v16l-6-4.2L6 20V4z"/></svg>
              <span class="ac-08__sr">Save this post</span>
            </button>
          </div>
        </div>
      </li>
      <li class="ac-08__card">
        <span class="ac-08__media"><img src="https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5?q=80&w=700&auto=format&fit=crop" alt="Blue abstract data light streaks" loading="lazy" decoding="async" width="700" height="560"></span>
        <div class="ac-08__body">
          <div class="ac-08__meta"><h3 class="ac-08__name">Signal, long exposure</h3><p class="ac-08__by">@kojiwrites</p></div>
          <div class="ac-08__acts">
            <button class="ac-08__like" type="button" data-like aria-pressed="false">
              <span class="ac-08__ring" aria-hidden="true"></span>
              <span class="ac-08__burst" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i></span>
              <svg class="ac-08__heart" viewBox="0 0 24 24" width="21" height="21" aria-hidden="true"><path d="M12 20.4l-1.5-1.36C5.4 14.4 2.4 11.7 2.4 8.4 2.4 5.9 4.4 4 6.9 4c1.5 0 2.9.7 3.8 1.8l1.3 1.5 1.3-1.5C14.2 4.7 15.6 4 17.1 4c2.5 0 4.5 1.9 4.5 4.4 0 3.3-3 6-8.1 10.66L12 20.4z"/></svg>
              <b class="ac-08__count" data-count>318</b>
              <span class="ac-08__sr" data-likesr>Like this post</span>
            </button>
            <button class="ac-08__save" type="button" data-save aria-pressed="false">
              <svg viewBox="0 0 24 24" width="19" height="19" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round" aria-hidden="true"><path d="M6 4h12v16l-6-4.2L6 20V4z"/></svg>
              <span class="ac-08__sr">Save this post</span>
            </button>
          </div>
        </div>
      </li>
    </ul>
    <p class="ac-08__sr" role="status" aria-live="polite" data-status></p>
    <p class="ac-08__chip" aria-hidden="true">rotate() then translateY() particle fan · scale overshoot 1.35 → .92 → 1 · aria-pressed toggle · CSS-only burst</p>
  </div>
</section>
.ac-08,
.ac-08 *,
.ac-08 *::before,
.ac-08 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.ac-08 {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  --ac-08-bg: oklch(0.15 0.015 320);
  --ac-08-card: oklch(0.20 0.018 320);
  --ac-08-ink: oklch(0.97 0.004 300);
  --ac-08-mut: oklch(0.69 0.02 320);
  --ac-08-acc: oklch(0.66 0.23 12);
  --ac-08-line: oklch(1 0 0/.11);
  background: var(--ac-08-bg);
  color: var(--ac-08-ink);
  font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
  -webkit-font-smoothing: antialiased;
}

.ac-08__stage {
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  place-items: center;
  align-content: center;
  gap: clamp(16px,2.6vw,26px);
  padding: clamp(20px,5vw,58px);
  background: radial-gradient(90% 60% at 50% 0%,color-mix(in oklch,var(--ac-08-acc) 12%,transparent),transparent 62%);
}

.ac-08__head {
  display: grid;
  justify-items: center;
  gap: 9px;
  text-align: center;
}

.ac-08__eyebrow {
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--ac-08-acc);
}

.ac-08__title {
  font-size: clamp(23px,3.4vw,38px);
  font-weight: 650;
  line-height: 1.12;
  letter-spacing: -.03em;
  text-wrap: balance;
}

.ac-08__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit,minmax(258px,1fr));
  gap: 14px;
  width: min(100%,700px);
  list-style: none;
}

.ac-08__card {
  display: grid;
  border: 1px solid var(--ac-08-line);
  border-radius: 20px;
  overflow: hidden;
  background: var(--ac-08-card);
}

.ac-08__media {
  display: block;
  aspect-ratio: 5/4;
  background: linear-gradient(135deg,oklch(0.36 0.08 340),oklch(0.26 0.05 300));
}

.ac-08__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.ac-08__body {
  display: grid;
  gap: 12px;
  padding: 14px 14px 12px;
}

.ac-08__meta {
  display: grid;
  gap: 3px;
}

.ac-08__name {
  font-size: 15px;
  font-weight: 640;
  letter-spacing: -.02em;
}

.ac-08__by {
  font-size: 12.5px;
  color: var(--ac-08-mut);
}

.ac-08__acts {
  display: flex;
  align-items: center;
  gap: 8px;
}

.ac-08__like {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 9px;
  min-height: 44px;
  padding: 0 16px;
  border: 1px solid var(--ac-08-line);
  border-radius: 99px;
  background: transparent;
  color: var(--ac-08-mut);
  font-family: inherit;
  cursor: pointer;
  transition: background .2s,color .2s,border-color .2s;
}

.ac-08__like:hover {
  background: oklch(1 0 0/.05);
  color: var(--ac-08-ink);
}

.ac-08__like:focus-visible {
  outline: 2px solid var(--ac-08-acc);
  outline-offset: 3px;
}

.ac-08__like[aria-pressed="true"] {
  color: var(--ac-08-acc);
  border-color: color-mix(in oklch,var(--ac-08-acc) 45%,transparent);
  background: color-mix(in oklch,var(--ac-08-acc) 12%,transparent);
}

.ac-08__heart {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  flex: none;
  transition: fill .2s;
}

.ac-08__like[aria-pressed="true"] .ac-08__heart {
  fill: var(--ac-08-acc);
  stroke: var(--ac-08-acc);
}

.ac-08__like[data-burst] .ac-08__heart {
  animation: ac-08-pop .55s cubic-bezier(.2,.7,.3,1);
}

@keyframes ac-08-pop {
  0% {
    scale: .2;
  }

  45% {
    scale: 1.35;
  }

  70% {
    scale: .92;
  }

  100% {
    scale: 1;
  }
}

.ac-08__count {
  font-size: 13.5px;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
  letter-spacing: -.01em;
}

.ac-08__ring {
  position: absolute;
  left: 16px;
  top: 50%;
  width: 21px;
  height: 21px;
  margin-top: -10.5px;
  border-radius: 50%;
  border: 2px solid var(--ac-08-acc);
  opacity: 0;
  pointer-events: none;
}

.ac-08__like[data-burst] .ac-08__ring {
  animation: ac-08-ring .6s cubic-bezier(.2,.7,.3,1);
}

@keyframes ac-08-ring {
  0% {
    opacity: .9;
    scale: .3;
  }

  70% {
    opacity: .25;
  }

  100% {
    opacity: 0;
    scale: 2.6;
  }
}

.ac-08__burst {
  position: absolute;
  left: 26.5px;
  top: 50%;
  width: 0;
  height: 0;
  pointer-events: none;
}

.ac-08__burst i {
  position: absolute;
  left: -3px;
  top: -3px;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--ac-08-acc);
  opacity: 0;
  transform: rotate(var(--ac-08-a)) translateY(0) scale(0);
}

.ac-08__burst i:nth-child(2) {
  --ac-08-a: 30deg;
  background: oklch(0.80 0.18 60);
}

.ac-08__burst i:nth-child(3) {
  --ac-08-a: 60deg;
}

.ac-08__burst i:nth-child(4) {
  --ac-08-a: 90deg;
  background: oklch(0.75 0.19 330);
}

.ac-08__burst i:nth-child(5) {
  --ac-08-a: 120deg;
}

.ac-08__burst i:nth-child(6) {
  --ac-08-a: 150deg;
  background: oklch(0.80 0.18 60);
}

.ac-08__burst i:nth-child(7) {
  --ac-08-a: 180deg;
}

.ac-08__burst i:nth-child(8) {
  --ac-08-a: 210deg;
  background: oklch(0.75 0.19 330);
}

.ac-08__burst i:nth-child(9) {
  --ac-08-a: 240deg;
}

.ac-08__burst i:nth-child(10) {
  --ac-08-a: 270deg;
  background: oklch(0.80 0.18 60);
}

.ac-08__burst i:nth-child(11) {
  --ac-08-a: 300deg;
}

.ac-08__burst i:nth-child(12) {
  --ac-08-a: 330deg;
  background: oklch(0.75 0.19 330);
}

.ac-08__burst i:nth-child(1) {
  --ac-08-a: 0deg;
}

.ac-08__like[data-burst] .ac-08__burst i {
  animation: ac-08-fly .62s cubic-bezier(.2,.7,.3,1) forwards;
}

.ac-08__like[data-burst] .ac-08__burst i:nth-child(even) {
  animation-duration: .72s;
  animation-delay: .04s;
}

@keyframes ac-08-fly {
  0% {
    opacity: 1;
    transform: rotate(var(--ac-08-a)) translateY(-4px) scale(0);
  }

  35% {
    opacity: 1;
    transform: rotate(var(--ac-08-a)) translateY(-24px) scale(1);
  }

  100% {
    opacity: 0;
    transform: rotate(var(--ac-08-a)) translateY(-38px) scale(0);
  }
}

.ac-08__save {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border: 1px solid var(--ac-08-line);
  border-radius: 99px;
  background: transparent;
  color: var(--ac-08-mut);
  cursor: pointer;
  transition: background .2s,color .2s,border-color .2s;
}

.ac-08__save:hover {
  background: oklch(1 0 0/.05);
  color: var(--ac-08-ink);
}

.ac-08__save:focus-visible {
  outline: 2px solid var(--ac-08-acc);
  outline-offset: 3px;
}

.ac-08__save[aria-pressed="true"] {
  color: oklch(0.85 0.16 88);
  border-color: color-mix(in oklch,oklch(0.85 0.16 88) 45%,transparent);
}

.ac-08__save[aria-pressed="true"] svg {
  fill: oklch(0.85 0.16 88);
  animation: ac-08-tuck .45s cubic-bezier(.2,.7,.3,1);
}

@keyframes ac-08-tuck {
  0% {
    translate: 0 -6px;
    scale: .8;
  }

  60% {
    translate: 0 2px;
  }

  100% {
    translate: 0 0;
    scale: 1;
  }
}

.ac-08__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.ac-08__chip {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11.5px;
  line-height: 1.6;
  color: var(--ac-08-mut);
  padding: 8px 15px;
  border: 1px solid var(--ac-08-line);
  border-radius: 99px;
  text-align: center;
  max-width: min(100%,700px);
}

@media (prefers-reduced-motion: reduce) {
  .ac-08__burst,
    .ac-08__ring {
    display: none;
  }

  .ac-08__like[data-burst] .ac-08__heart {
    animation: none;
  }

  .ac-08__save[aria-pressed="true"] svg {
    animation: none;
  }
}
(() => {
  const nf = new Intl.NumberFormat('en-US');
  document.querySelectorAll('.ac-08').forEach((root) => {
    if (root.dataset.wired) return;
    root.dataset.wired = '1';
    const status = root.querySelector('[data-status]');

    root.querySelectorAll('[data-like]').forEach((btn) => {
      const out = btn.querySelector('[data-count]');
      const sr = btn.querySelector('[data-likesr]');
      let n = +out.textContent.replace(/,/g, '');
      btn.addEventListener('click', () => {
        const on = btn.getAttribute('aria-pressed') !== 'true';
        btn.setAttribute('aria-pressed', String(on));
        n += on ? 1 : -1;
        out.textContent = nf.format(n);
        sr.textContent = on ? 'Unlike this post' : 'Like this post';
        if (on) {
          btn.removeAttribute('data-burst');
          void btn.offsetWidth;                 // reflow so the keyframes restart on re-like
          btn.dataset.burst = '';
        } else {
          btn.removeAttribute('data-burst');    // undo gets no celebration
        }
        status.textContent = (on ? 'Liked. ' : 'Like removed. ') + nf.format(n) + ' likes.';
      });
    });

    root.querySelectorAll('[data-save]').forEach((btn) => {
      btn.addEventListener('click', () => {
        const on = btn.getAttribute('aria-pressed') !== 'true';
        btn.setAttribute('aria-pressed', String(on));
        status.textContent = on ? 'Saved to your collection.' : 'Removed from your collection.';
      });
    });
  });
})();
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Animated Card from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Like/Save Burst Micro-Interaction
Source: https://codefronts.com/components/css-animated-cards/like-save-burst-micro-interaction/

The like that feels good to press: an overshooting heart, twelve particles thrown out on trigonometric angles, an expanding ring, and a count that rolls over — all CSS keyframes fired by a single class toggle. The particles are pure CSS (no canvas, no library), positioned with a rotate-then-translate trick so twelve elements need one keyframe between them.
## HTML
```html
<section class="ac-08" aria-label="Like and save burst micro-interaction demo">
  <div class="ac-08__stage">
    <header class="ac-08__head">
      <p class="ac-08__eyebrow">Micro-interaction</p>
      <h2 class="ac-08__title">A like worth pressing twice</h2>
    </header>
    <ul class="ac-08__grid" role="list">
      <li class="ac-08__card">
        <span class="ac-08__media"><img src="https://images.unsplash.com/photo-1517841905240-472988babdf9?q=80&w=700&auto=format&fit=crop" alt="Portrait of a woman in warm light" loading="lazy" decoding="async" width="700" height="560"></span>
        <div class="ac-08__body">
          <div class="ac-08__meta"><h3 class="ac-08__name">Golden hour, no filter</h3><p class="ac-08__by">@rennadavis</p></div>
          <div class="ac-08__acts">
            <button class="ac-08__like" type="button" data-like aria-pressed="false">
              <span class="ac-08__ring" aria-hidden="true"></span>
              <span class="ac-08__burst" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i></span>
              <svg class="ac-08__heart" viewBox="0 0 24 24" width="21" height="21" aria-hidden="true"><path d="M12 20.4l-1.5-1.36C5.4 14.4 2.4 11.7 2.4 8.4 2.4 5.9 4.4 4 6.9 4c1.5 0 2.9.7 3.8 1.8l1.3 1.5 1.3-1.5C14.2 4.7 15.6 4 17.1 4c2.5 0 4.5 1.9 4.5 4.4 0 3.3-3 6-8.1 10.66L12 20.4z"/></svg>
              <b class="ac-08__count" data-count>1,204</b>
              <span class="ac-08__sr" data-likesr>Like this post</span>
            </button>
            <button class="ac-08__save" type="button" data-save aria-pressed="false">
              <svg viewBox="0 0 24 24" width="19" height="19" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round" aria-hidden="true"><path d="M6 4h12v16l-6-4.2L6 20V4z"/></svg>
              <span class="ac-08__sr">Save this post</span>
            </button>
          </div>
        </div>
      </li>
      <li class="ac-08__card">
        <span class="ac-08__media"><img src="https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5?q=80&w=700&auto=format&fit=crop" alt="Blue abstract data light streaks" loading="lazy" decoding="async" width="700" height="560"></span>
        <div class="ac-08__body">
          <div class="ac-08__meta"><h3 class="ac-08__name">Signal, long exposure</h3><p class="ac-08__by">@kojiwrites</p></div>
          <div class="ac-08__acts">
            <button class="ac-08__like" type="button" data-like aria-pressed="false">
              <span class="ac-08__ring" aria-hidden="true"></span>
              <span class="ac-08__burst" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i></span>
              <svg class="ac-08__heart" viewBox="0 0 24 24" width="21" height="21" aria-hidden="true"><path d="M12 20.4l-1.5-1.36C5.4 14.4 2.4 11.7 2.4 8.4 2.4 5.9 4.4 4 6.9 4c1.5 0 2.9.7 3.8 1.8l1.3 1.5 1.3-1.5C14.2 4.7 15.6 4 17.1 4c2.5 0 4.5 1.9 4.5 4.4 0 3.3-3 6-8.1 10.66L12 20.4z"/></svg>
              <b class="ac-08__count" data-count>318</b>
              <span class="ac-08__sr" data-likesr>Like this post</span>
            </button>
            <button class="ac-08__save" type="button" data-save aria-pressed="false">
              <svg viewBox="0 0 24 24" width="19" height="19" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round" aria-hidden="true"><path d="M6 4h12v16l-6-4.2L6 20V4z"/></svg>
              <span class="ac-08__sr">Save this post</span>
            </button>
          </div>
        </div>
      </li>
    </ul>
    <p class="ac-08__sr" role="status" aria-live="polite" data-status></p>
    <p class="ac-08__chip" aria-hidden="true">rotate() then translateY() particle fan · scale overshoot 1.35 → .92 → 1 · aria-pressed toggle · CSS-only burst</p>
  </div>
</section>
```
## CSS
```css
.ac-08,
.ac-08 *,
.ac-08 *::before,
.ac-08 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.ac-08 {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  --ac-08-bg: oklch(0.15 0.015 320);
  --ac-08-card: oklch(0.20 0.018 320);
  --ac-08-ink: oklch(0.97 0.004 300);
  --ac-08-mut: oklch(0.69 0.02 320);
  --ac-08-acc: oklch(0.66 0.23 12);
  --ac-08-line: oklch(1 0 0/.11);
  background: var(--ac-08-bg);
  color: var(--ac-08-ink);
  font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
  -webkit-font-smoothing: antialiased;
}

.ac-08__stage {
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  place-items: center;
  align-content: center;
  gap: clamp(16px,2.6vw,26px);
  padding: clamp(20px,5vw,58px);
  background: radial-gradient(90% 60% at 50% 0%,color-mix(in oklch,var(--ac-08-acc) 12%,transparent),transparent 62%);
}

.ac-08__head {
  display: grid;
  justify-items: center;
  gap: 9px;
  text-align: center;
}

.ac-08__eyebrow {
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--ac-08-acc);
}

.ac-08__title {
  font-size: clamp(23px,3.4vw,38px);
  font-weight: 650;
  line-height: 1.12;
  letter-spacing: -.03em;
  text-wrap: balance;
}

.ac-08__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit,minmax(258px,1fr));
  gap: 14px;
  width: min(100%,700px);
  list-style: none;
}

.ac-08__card {
  display: grid;
  border: 1px solid var(--ac-08-line);
  border-radius: 20px;
  overflow: hidden;
  background: var(--ac-08-card);
}

.ac-08__media {
  display: block;
  aspect-ratio: 5/4;
  background: linear-gradient(135deg,oklch(0.36 0.08 340),oklch(0.26 0.05 300));
}

.ac-08__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.ac-08__body {
  display: grid;
  gap: 12px;
  padding: 14px 14px 12px;
}

.ac-08__meta {
  display: grid;
  gap: 3px;
}

.ac-08__name {
  font-size: 15px;
  font-weight: 640;
  letter-spacing: -.02em;
}

.ac-08__by {
  font-size: 12.5px;
  color: var(--ac-08-mut);
}

.ac-08__acts {
  display: flex;
  align-items: center;
  gap: 8px;
}

.ac-08__like {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 9px;
  min-height: 44px;
  padding: 0 16px;
  border: 1px solid var(--ac-08-line);
  border-radius: 99px;
  background: transparent;
  color: var(--ac-08-mut);
  font-family: inherit;
  cursor: pointer;
  transition: background .2s,color .2s,border-color .2s;
}

.ac-08__like:hover {
  background: oklch(1 0 0/.05);
  color: var(--ac-08-ink);
}

.ac-08__like:focus-visible {
  outline: 2px solid var(--ac-08-acc);
  outline-offset: 3px;
}

.ac-08__like[aria-pressed="true"] {
  color: var(--ac-08-acc);
  border-color: color-mix(in oklch,var(--ac-08-acc) 45%,transparent);
  background: color-mix(in oklch,var(--ac-08-acc) 12%,transparent);
}

.ac-08__heart {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  flex: none;
  transition: fill .2s;
}

.ac-08__like[aria-pressed="true"] .ac-08__heart {
  fill: var(--ac-08-acc);
  stroke: var(--ac-08-acc);
}

.ac-08__like[data-burst] .ac-08__heart {
  animation: ac-08-pop .55s cubic-bezier(.2,.7,.3,1);
}

@keyframes ac-08-pop {
  0% {
    scale: .2;
  }

  45% {
    scale: 1.35;
  }

  70% {
    scale: .92;
  }

  100% {
    scale: 1;
  }
}

.ac-08__count {
  font-size: 13.5px;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
  letter-spacing: -.01em;
}

.ac-08__ring {
  position: absolute;
  left: 16px;
  top: 50%;
  width: 21px;
  height: 21px;
  margin-top: -10.5px;
  border-radius: 50%;
  border: 2px solid var(--ac-08-acc);
  opacity: 0;
  pointer-events: none;
}

.ac-08__like[data-burst] .ac-08__ring {
  animation: ac-08-ring .6s cubic-bezier(.2,.7,.3,1);
}

@keyframes ac-08-ring {
  0% {
    opacity: .9;
    scale: .3;
  }

  70% {
    opacity: .25;
  }

  100% {
    opacity: 0;
    scale: 2.6;
  }
}

.ac-08__burst {
  position: absolute;
  left: 26.5px;
  top: 50%;
  width: 0;
  height: 0;
  pointer-events: none;
}

.ac-08__burst i {
  position: absolute;
  left: -3px;
  top: -3px;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--ac-08-acc);
  opacity: 0;
  transform: rotate(var(--ac-08-a)) translateY(0) scale(0);
}

.ac-08__burst i:nth-child(2) {
  --ac-08-a: 30deg;
  background: oklch(0.80 0.18 60);
}

.ac-08__burst i:nth-child(3) {
  --ac-08-a: 60deg;
}

.ac-08__burst i:nth-child(4) {
  --ac-08-a: 90deg;
  background: oklch(0.75 0.19 330);
}

.ac-08__burst i:nth-child(5) {
  --ac-08-a: 120deg;
}

.ac-08__burst i:nth-child(6) {
  --ac-08-a: 150deg;
  background: oklch(0.80 0.18 60);
}

.ac-08__burst i:nth-child(7) {
  --ac-08-a: 180deg;
}

.ac-08__burst i:nth-child(8) {
  --ac-08-a: 210deg;
  background: oklch(0.75 0.19 330);
}

.ac-08__burst i:nth-child(9) {
  --ac-08-a: 240deg;
}

.ac-08__burst i:nth-child(10) {
  --ac-08-a: 270deg;
  background: oklch(0.80 0.18 60);
}

.ac-08__burst i:nth-child(11) {
  --ac-08-a: 300deg;
}

.ac-08__burst i:nth-child(12) {
  --ac-08-a: 330deg;
  background: oklch(0.75 0.19 330);
}

.ac-08__burst i:nth-child(1) {
  --ac-08-a: 0deg;
}

.ac-08__like[data-burst] .ac-08__burst i {
  animation: ac-08-fly .62s cubic-bezier(.2,.7,.3,1) forwards;
}

.ac-08__like[data-burst] .ac-08__burst i:nth-child(even) {
  animation-duration: .72s;
  animation-delay: .04s;
}

@keyframes ac-08-fly {
  0% {
    opacity: 1;
    transform: rotate(var(--ac-08-a)) translateY(-4px) scale(0);
  }

  35% {
    opacity: 1;
    transform: rotate(var(--ac-08-a)) translateY(-24px) scale(1);
  }

  100% {
    opacity: 0;
    transform: rotate(var(--ac-08-a)) translateY(-38px) scale(0);
  }
}

.ac-08__save {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border: 1px solid var(--ac-08-line);
  border-radius: 99px;
  background: transparent;
  color: var(--ac-08-mut);
  cursor: pointer;
  transition: background .2s,color .2s,border-color .2s;
}

.ac-08__save:hover {
  background: oklch(1 0 0/.05);
  color: var(--ac-08-ink);
}

.ac-08__save:focus-visible {
  outline: 2px solid var(--ac-08-acc);
  outline-offset: 3px;
}

.ac-08__save[aria-pressed="true"] {
  color: oklch(0.85 0.16 88);
  border-color: color-mix(in oklch,oklch(0.85 0.16 88) 45%,transparent);
}

.ac-08__save[aria-pressed="true"] svg {
  fill: oklch(0.85 0.16 88);
  animation: ac-08-tuck .45s cubic-bezier(.2,.7,.3,1);
}

@keyframes ac-08-tuck {
  0% {
    translate: 0 -6px;
    scale: .8;
  }

  60% {
    translate: 0 2px;
  }

  100% {
    translate: 0 0;
    scale: 1;
  }
}

.ac-08__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.ac-08__chip {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11.5px;
  line-height: 1.6;
  color: var(--ac-08-mut);
  padding: 8px 15px;
  border: 1px solid var(--ac-08-line);
  border-radius: 99px;
  text-align: center;
  max-width: min(100%,700px);
}

@media (prefers-reduced-motion: reduce) {
  .ac-08__burst,
    .ac-08__ring {
    display: none;
  }

  .ac-08__like[data-burst] .ac-08__heart {
    animation: none;
  }

  .ac-08__save[aria-pressed="true"] svg {
    animation: none;
  }
}
```

## JavaScript
```js
(() => {
  const nf = new Intl.NumberFormat('en-US');
  document.querySelectorAll('.ac-08').forEach((root) => {
    if (root.dataset.wired) return;
    root.dataset.wired = '1';
    const status = root.querySelector('[data-status]');

    root.querySelectorAll('[data-like]').forEach((btn) => {
      const out = btn.querySelector('[data-count]');
      const sr = btn.querySelector('[data-likesr]');
      let n = +out.textContent.replace(/,/g, '');
      btn.addEventListener('click', () => {
        const on = btn.getAttribute('aria-pressed') !== 'true';
        btn.setAttribute('aria-pressed', String(on));
        n += on ? 1 : -1;
        out.textContent = nf.format(n);
        sr.textContent = on ? 'Unlike this post' : 'Like this post';
        if (on) {
          btn.removeAttribute('data-burst');
          void btn.offsetWidth;                 // reflow so the keyframes restart on re-like
          btn.dataset.burst = '';
        } else {
          btn.removeAttribute('data-burst');    // undo gets no celebration
        }
        status.textContent = (on ? 'Liked. ' : 'Like removed. ') + nf.format(n) + ' likes.';
      });
    });

    root.querySelectorAll('[data-save]').forEach((btn) => {
      btn.addEventListener('click', () => {
        const on = btn.getAttribute('aria-pressed') !== 'true';
        btn.setAttribute('aria-pressed', String(on));
        status.textContent = on ? 'Saved to your collection.' : 'Removed from your collection.';
      });
    });
  });
})();
```

How this works

Every particle uses the same keyframe; only its angle differs, supplied as a custom property:

.p { --a: 0deg;
     transform: rotate(var(--a)) translateY(0) scale(0);
     animation: ac-08-burst .62s cubic-bezier(.2,.7,.3,1) forwards }
.p:nth-child(2) { --a: 30deg } /* … 12 × 30° */
@keyframes ac-08-burst {
  30%  { transform: rotate(var(--a)) translateY(-26px) scale(1) }
  100% { transform: rotate(var(--a)) translateY(-40px) scale(0); opacity: 0 }
}

Rotate first, then translate along the rotated Y axis: that is how you fan elements around a circle without twelve hand-written translate pairs.

The heart itself overshoots — scale(0) → 1.35 → 0.92 → 1 on a snappy bezier. That second, smaller bounce is what separates a delightful toggle from a mechanical one; a single ease-out to 1 always reads as flat.

@keyframes ac-08-pop { 0%{scale:0} 45%{scale:1.35} 70%{scale:.92} 100%{scale:1} }

State is aria-pressed on a real <button>, and the count is in a polite live region that announces the total, not the animation. Un-liking plays a quiet reverse — you should never punish an undo with the full celebration.

Make it yours

  • Bookmark instead of heart: swap the SVG path and the burst colour; the choreography is identical for save, star, upvote and reaction buttons.
  • Randomise particle distance with --d: calc(30px + var(--r) * 14px) and set --r per particle for a less mechanical spray.
  • Long-press for a reaction picker: on pointerdown start a 400 ms timer that opens a popover of emoji-free reaction icons.
  • Double-tap to like on touch: listen for two pointerup events within 280 ms on the media area, and fire the same burst at the tap coordinates.

Gotchas — read before shipping

  • Restarting a CSS animation requires removing the class, forcing reflow (void el.offsetWidth), then re-adding it — or the second click does nothing.
  • Twelve absolutely-positioned particles must be pointer-events: none, or they intercept the click that spawned them.
  • Use aria-pressed, never a visual-only state. And never put aria-live on the button itself — the label re-announces on every render.
  • Optimistic UI needs a rollback: increment locally, and on request failure decrement and show a quiet error — do not replay the burst.
  • Under prefers-reduced-motion, drop the particles entirely and keep a 120 ms colour fill. The information (liked) survives; the confetti does not.

Browser support

ChromeSafariFirefoxEdge
114+17.5+121+114+

Floor set by oklch(), color-mix(), text-wrap as this demo is written. The core technique itself goes back further — Chrome 88+.

Custom properties in transforms, individual transform properties (scale/rotate/translate) and keyframe overshoot are all Baseline Widely Available. Individual transform properties need Chrome 104 / Safari 14.1 / Firefox 72.

Techniques used in this demo

Search CodeFronts

Loading…