16 CSS Pulse Animations14 / 16

Pure CSSMIT licensed

Pulsing Heart Icon

A wishlist / save-for-later heart button on a product card — the heart pulses with a natural cardiac rhythm (not a mechanical scale), giving the interaction warmth and personality. Warm pink card (#fecaca to #fff1f2) with crimson heart (#dc2626) and a small saved-count badge. Pinterest / Airbnb / Instagram save button aesthetic. The keyframe uses cardiac timing (0% → 20% quick scale-up → 40% pull-back → 60% settle) that mimics a real heartbeat's systole-diastole rhythm — the reason this pattern feels alive, not robotic.

Published

Live Demo
Try it

The code

<div class="pl-14">
  <article class="pl-14__card">
    <div class="pl-14__image" role="img" aria-label="Artisan hand-poured soy candle, verbena and cedarwood">
      <span aria-hidden="true">🕯️</span>
    </div>
    <button class="pl-14__save" data-saved="true" aria-label="Saved · 3,200 people saved this" aria-pressed="true">
      <svg class="pl-14__heart" viewBox="0 0 24 24" width="20" height="20" fill="currentColor" aria-hidden="true">
        <path d="M12 21s-8-4.5-8-11a5 5 0 0 1 9-3 5 5 0 0 1 9 3c0 6.5-8 11-8 11H12z"/>
      </svg>
    </button>
    <div class="pl-14__details">
      <p class="pl-14__brand">Rowan &amp; Poppy</p>
      <h3 class="pl-14__name">Verbena, cedarwood &amp; sea salt</h3>
      <p class="pl-14__price">
        <span>$42</span>
        <span class="pl-14__unit">· 8oz hand-poured soy candle</span>
      </p>
      <p class="pl-14__saved">
        <svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor" aria-hidden="true">
          <path d="M12 21s-8-4.5-8-11a5 5 0 0 1 9-3 5 5 0 0 1 9 3c0 6.5-8 11-8 11H12z"/>
        </svg>
        <strong>3,247</strong> saved · <strong>412</strong> in the last week
      </p>
    </div>
  </article>
</div>
.pl-14 {
  --page-bg: #fdf2f8;
  --card-a: #fff1f2;
  --card-b: #fecaca;
  --ink: #4c0519;
  --sub: #8b5a68;
  --brand: #dc2626;
  --brand-soft: rgba(220,38,38,.35);
  --gold: #a67c3a;
  --line: #fecdd3;
  font-family: -apple-system,BlinkMacSystemFont,'Inter','Segoe UI',sans-serif;
  width: 100%;
  min-height: 100vh;
  position: relative;
  display: grid;
  place-items: center;
  padding: 32px;
  background: radial-gradient(70% 90% at 30% 20%, rgba(254,205,211,.6) 0%, transparent 60%), var(--page-bg);
  color: var(--ink);
}

.pl-14 *,
.pl-14 *::before,
.pl-14 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.pl-14 ::selection {
  background: var(--brand);
  color: #fff;
}

.pl-14__card {
  position: relative;
  display: grid;
  grid-template-columns: 200px 1fr;
  gap: 22px;
  width: min(560px,100%);
  padding: 22px;
  border-radius: 16px;
  background: linear-gradient(135deg,var(--card-a) 0%,var(--card-b) 100%);
  border: 1px solid rgba(220,38,38,.15);
  box-shadow: 0 20px 40px -20px rgba(76,5,25,.25);
}

@media (max-width: 520px) {
  .pl-14__card {
    grid-template-columns: 1fr;
  }
}

.pl-14__image {
  aspect-ratio: 1/1;
  border-radius: 11px;
  background: linear-gradient(135deg,#f9a8d4,#ec4899 100%);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 76px;
  box-shadow: inset 0 0 40px rgba(76,5,25,.15);
}
/* Save button pinned to card top-right */

.pl-14__save {
  position: absolute;
  top: 14px;
  right: 14px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border: none;
  border-radius: 50%;
  background: rgba(255,255,255,.9);
  color: var(--brand);
  cursor: pointer;
  box-shadow: 0 4px 12px -4px rgba(76,5,25,.25);
  backdrop-filter: blur(4px);
  transition: background .15s ease,transform .12s ease;
}

.pl-14__save:hover {
  background: #fff;
  transform: translateY(-1px);
}

.pl-14__save:active {
  transform: translateY(1px);
}

.pl-14__save:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 3px;
}
/* Glow ring behind heart — scales with heartbeat */

.pl-14__save::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: var(--brand-soft);
  pointer-events: none;
  animation: pl-14-glow 1.6s ease-in-out infinite;
  z-index: 0;
}

.pl-14__heart {
  position: relative;
  z-index: 1;
  animation: pl-14-heart 1.6s ease-in-out infinite;
  transform-origin: center;
}
/* Cardiac rhythm — hand-tuned keyframe mimics QRS complex */

@keyframes pl-14-heart {
  0% {
    transform: scale(1);
  }

  15% {
    transform: scale(1.22);
  }

  30% {
    transform: scale(1.05);
  }

  45% {
    transform: scale(1.15);
  }

  60% {
    transform: scale(1);
  }

  100% {
    transform: scale(1);
  }
}

@keyframes pl-14-glow {
  0% {
    transform: scale(1);
    opacity: .5;
  }

  15% {
    transform: scale(1.5);
    opacity: 0;
  }

  100% {
    transform: scale(1.5);
    opacity: 0;
  }
}

.pl-14__details {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.pl-14__brand {
  font-size: .7rem;
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--gold);
}

.pl-14__name {
  font-family: 'Playfair Display',Georgia,serif;
  font-size: 1.15rem;
  font-weight: 800;
  letter-spacing: -.015em;
  line-height: 1.2;
  color: var(--ink);
  margin-bottom: 2px;
}

.pl-14__price {
  font-size: .85rem;
  font-weight: 700;
  color: var(--ink);
}

.pl-14__price > span:first-child {
  color: var(--brand);
  font-size: 1.05rem;
}

.pl-14__unit {
  color: var(--sub);
  font-weight: 500;
}

.pl-14__saved {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: .75rem;
  color: var(--sub);
  margin-top: auto;
  padding-top: 8px;
  border-top: 1px solid var(--line);
}

.pl-14__saved svg {
  color: var(--brand);
}

.pl-14__saved strong {
  color: var(--ink);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}

@media (prefers-reduced-motion: reduce) {
  .pl-14__heart,
    .pl-14__save::before {
    animation: none!important;
    transform: none!important;
    opacity: 0!important;
  }

  .pl-14__save::before {
    display: none!important;
  }
}
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 Pulse Animation 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: Pulsing Heart Icon
Source: https://codefronts.com/motion/css-pulse-animation/pulsing-heart-icon/

A wishlist / save-for-later heart button on a product card — the heart pulses with a natural cardiac rhythm (not a mechanical scale), giving the interaction warmth and personality. Warm pink card (#fecaca to #fff1f2) with crimson heart (#dc2626) and a small saved-count badge. Pinterest / Airbnb / Instagram save button aesthetic. The keyframe uses cardiac timing (0% → 20% quick scale-up → 40% pull-back → 60% settle) that mimics a real heartbeat's systole-diastole rhythm — the reason this pattern feels alive, not robotic.
## HTML
```html
<div class="pl-14">
  <article class="pl-14__card">
    <div class="pl-14__image" role="img" aria-label="Artisan hand-poured soy candle, verbena and cedarwood">
      <span aria-hidden="true">🕯️</span>
    </div>
    <button class="pl-14__save" data-saved="true" aria-label="Saved · 3,200 people saved this" aria-pressed="true">
      <svg class="pl-14__heart" viewBox="0 0 24 24" width="20" height="20" fill="currentColor" aria-hidden="true">
        <path d="M12 21s-8-4.5-8-11a5 5 0 0 1 9-3 5 5 0 0 1 9 3c0 6.5-8 11-8 11H12z"/>
      </svg>
    </button>
    <div class="pl-14__details">
      <p class="pl-14__brand">Rowan &amp; Poppy</p>
      <h3 class="pl-14__name">Verbena, cedarwood &amp; sea salt</h3>
      <p class="pl-14__price">
        <span>$42</span>
        <span class="pl-14__unit">· 8oz hand-poured soy candle</span>
      </p>
      <p class="pl-14__saved">
        <svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor" aria-hidden="true">
          <path d="M12 21s-8-4.5-8-11a5 5 0 0 1 9-3 5 5 0 0 1 9 3c0 6.5-8 11-8 11H12z"/>
        </svg>
        <strong>3,247</strong> saved · <strong>412</strong> in the last week
      </p>
    </div>
  </article>
</div>
```
## CSS
```css
.pl-14 {
  --page-bg: #fdf2f8;
  --card-a: #fff1f2;
  --card-b: #fecaca;
  --ink: #4c0519;
  --sub: #8b5a68;
  --brand: #dc2626;
  --brand-soft: rgba(220,38,38,.35);
  --gold: #a67c3a;
  --line: #fecdd3;
  font-family: -apple-system,BlinkMacSystemFont,'Inter','Segoe UI',sans-serif;
  width: 100%;
  min-height: 100vh;
  position: relative;
  display: grid;
  place-items: center;
  padding: 32px;
  background: radial-gradient(70% 90% at 30% 20%, rgba(254,205,211,.6) 0%, transparent 60%), var(--page-bg);
  color: var(--ink);
}

.pl-14 *,
.pl-14 *::before,
.pl-14 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.pl-14 ::selection {
  background: var(--brand);
  color: #fff;
}

.pl-14__card {
  position: relative;
  display: grid;
  grid-template-columns: 200px 1fr;
  gap: 22px;
  width: min(560px,100%);
  padding: 22px;
  border-radius: 16px;
  background: linear-gradient(135deg,var(--card-a) 0%,var(--card-b) 100%);
  border: 1px solid rgba(220,38,38,.15);
  box-shadow: 0 20px 40px -20px rgba(76,5,25,.25);
}

@media (max-width: 520px) {
  .pl-14__card {
    grid-template-columns: 1fr;
  }
}

.pl-14__image {
  aspect-ratio: 1/1;
  border-radius: 11px;
  background: linear-gradient(135deg,#f9a8d4,#ec4899 100%);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 76px;
  box-shadow: inset 0 0 40px rgba(76,5,25,.15);
}
/* Save button pinned to card top-right */

.pl-14__save {
  position: absolute;
  top: 14px;
  right: 14px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border: none;
  border-radius: 50%;
  background: rgba(255,255,255,.9);
  color: var(--brand);
  cursor: pointer;
  box-shadow: 0 4px 12px -4px rgba(76,5,25,.25);
  backdrop-filter: blur(4px);
  transition: background .15s ease,transform .12s ease;
}

.pl-14__save:hover {
  background: #fff;
  transform: translateY(-1px);
}

.pl-14__save:active {
  transform: translateY(1px);
}

.pl-14__save:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 3px;
}
/* Glow ring behind heart — scales with heartbeat */

.pl-14__save::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: var(--brand-soft);
  pointer-events: none;
  animation: pl-14-glow 1.6s ease-in-out infinite;
  z-index: 0;
}

.pl-14__heart {
  position: relative;
  z-index: 1;
  animation: pl-14-heart 1.6s ease-in-out infinite;
  transform-origin: center;
}
/* Cardiac rhythm — hand-tuned keyframe mimics QRS complex */

@keyframes pl-14-heart {
  0% {
    transform: scale(1);
  }

  15% {
    transform: scale(1.22);
  }

  30% {
    transform: scale(1.05);
  }

  45% {
    transform: scale(1.15);
  }

  60% {
    transform: scale(1);
  }

  100% {
    transform: scale(1);
  }
}

@keyframes pl-14-glow {
  0% {
    transform: scale(1);
    opacity: .5;
  }

  15% {
    transform: scale(1.5);
    opacity: 0;
  }

  100% {
    transform: scale(1.5);
    opacity: 0;
  }
}

.pl-14__details {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.pl-14__brand {
  font-size: .7rem;
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--gold);
}

.pl-14__name {
  font-family: 'Playfair Display',Georgia,serif;
  font-size: 1.15rem;
  font-weight: 800;
  letter-spacing: -.015em;
  line-height: 1.2;
  color: var(--ink);
  margin-bottom: 2px;
}

.pl-14__price {
  font-size: .85rem;
  font-weight: 700;
  color: var(--ink);
}

.pl-14__price > span:first-child {
  color: var(--brand);
  font-size: 1.05rem;
}

.pl-14__unit {
  color: var(--sub);
  font-weight: 500;
}

.pl-14__saved {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: .75rem;
  color: var(--sub);
  margin-top: auto;
  padding-top: 8px;
  border-top: 1px solid var(--line);
}

.pl-14__saved svg {
  color: var(--brand);
}

.pl-14__saved strong {
  color: var(--ink);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}

@media (prefers-reduced-motion: reduce) {
  .pl-14__heart,
    .pl-14__save::before {
    animation: none!important;
    transform: none!important;
    opacity: 0!important;
  }

  .pl-14__save::before {
    display: none!important;
  }
}
```

How this works

The heart is a pure SVG path (inline in HTML, no icon font, no library) — two Bézier curves forming the classic heart shape. The path fill is white in the resting state and toggles to crimson (#dc2626) via a [data-saved="true"] attribute. In this demo the heart is pre-saved, so it's shown in the crimson state for the pulse to be visible.

The pulse uses a hand-tuned cardiac-rhythm keyframe: 0% scale(1) → 15% scale(1.22) → 30% scale(1.05) → 45% scale(1.15) → 60% scale(1) → 100% scale(1). This 4-beat pattern (up, down, up-again-slightly, settle) mimics the QRS complex of a real heartbeat — a strong contraction (systole), a brief relaxation, a smaller second contraction (dicrotic notch), then quiet (diastole). Cycle is 1.6 seconds — the resting HR of a calm adult. This is the difference between a heart icon feeling ALIVE and one feeling ROBOTIC.

A soft glow ring appears behind the heart via ::before pseudo-element — pink glow that scales 1 → 1.6 with opacity .5 → 0 in sync with the heart's peak contraction. The glow is subtle enough to feel like radiance, not intrusive enough to feel like an alert.

Product card layout with saved-heart button in the top-right corner: image thumbnail (mock artisan candle emoji), product name, category, price, saved counter ("3.2k saved") below. Warm pink gradient card surface (#fff1f2#fecaca) — Airbnb-adjacent save aesthetic, not fintech-red. Serif font (Playfair Display) for product name gives it the boutique feel; sans for metadata.

Make it yours

  • Change the pulse timing to convey urgency — 1s cycle for excited / high-engagement contexts ("someone just saved this!"), 2s for calm ambient wishlist state. Below 0.8s reads as "panicked heartbeat" which is off-brand.
  • Add a click-triggered burst — on :active, the heart does a single dramatic scale to 1.4 with a particle-burst effect (small dots exploding outward). Use pseudo-elements + animation-iteration-count: 1.
  • For unsaved state (empty outline heart), disable the pulse entirely — only the SAVED heart pulses. Toggle animation: none when [data-saved="false"].
  • Change to a bookmark or star icon for non-heart wishlists — the same cardiac rhythm keyframe works because it feels alive on ANY shape. Just replace the SVG path.
  • For social contexts ("3 friends also saved this"), add small avatar dots below the heart — the pulse draws attention, the avatars provide social proof.

Gotchas — read before shipping

  • The heart's peak scale MUST stay under 1.3 — larger scales trigger visible pixelation on the SVG stroke edges. Use 1.15-1.25 for smooth-looking peaks.
  • For accessibility, the button must have aria-label="Saved · 3,200 people saved this" or aria-pressed="true" reflecting the toggle state. Screen readers need to know the heart is currently activated.
  • Do NOT sync the pulse to Math.random() or setInterval jitter to look "more organic" — a synchronized rhythm actually reads as ALIVE. Randomization looks broken.
  • For platforms with many hearts on one page (product gallery, wishlist page), reserve the pulse for the MOST RECENTLY saved heart only. Ambient pulse on 40 hearts creates attention competition.
  • The cardiac rhythm keyframe (multiple stops within one cycle) is more expensive than a simple 2-stop pulse. Cap concurrent hearts to 10-15; for high-density lists, use a simpler single-scale keyframe.
  • Do NOT animate the heart's fill COLOR alongside the scale — color changes on every frame trigger paint work. Scale is compositor-safe; color transitions on save/unsave should happen INSTANTLY (not tweened).
  • For prefers-reduced-motion: keep the heart VISIBLE in its saturated crimson state (conveying "saved"), disable the pulse animation. The color alone conveys the state adequately.

Browser support

ChromeSafariFirefoxEdge
76+9+103+76+

Floor set by backdrop-filter as this demo is written. The core technique itself goes back further — Chrome 45+.

transform + opacity + @keyframes. Universal, GPU-accelerated.

Techniques used in this demo

Search CodeFronts

Loading…