Like Heart
A heart button that fires three simultaneous CSS animations on click: the heart pops with an elastic overshoot, a ring spreads out, and eight colored particles burst in a star pattern.
Updated
This effect is free — the Hover category contains 48 effects total, including 5 free. Effect.Labs has 840 vanilla effects. Explore the category →
Usage examples
Japanese Maple — Acer palmatum
Ornamental deciduous tree with palmate leaves, 5–9 lobes. Leaves turn bright red in autumn. Hardy in zones 5–8. Avoid waterlogging.
How it works
The .fx-0361-root root (250 px tall, rgb(26, 26, 36) background) centers a column made up of the button and the counter. The .mi-heart-btn button is an 80 px circle. It holds the heart SVG, eight absolute span.mi-heart-particle elements (each positioned at 50% / 50% so they depart from the center) and an ::after pseudo-element for the ring. All animations are triggered by the .liked class being present on the button — no JavaScript beyond toggling that class.
Three CSS animations fire on click. @keyframes miHeartPop (0.4 s) scales the heart to 1.3× at 30%, pulls it back to 0.9× at 60%, then returns to 1 with a cubic-bezier(0.17, 0.89, 0.32, 1.49) that gives the bounce. @keyframes miHeartRing (0.6 s) expands the ::after from 0.8× to 1.4× as it fades out. @keyframes miParticleBurst (0.6 s) propels each span along an axis set by the --angle custom property (0°, 45°, 90°… 315°). The adjacent counter turns red as soon as .liked is present (CSS ~ .mi-heart-count sibling selector).
The script finds every .fx-0361-root on the page, adds a click listener to its button, and manages aria-pressed: “false” by default, “true” when .liked is present. Each copy of the component is isolated from the others.
Accessibility
- Keyboard: the component is a real
<button>; Tab reaches it, Space or Enter fires it. The focus outline (2 px indigo) only appears with keyboard navigation. - Announced state:
aria-pressedis updated on every click. A screen reader announces the button as not pressed at rest and pressed once liked. Make sure thearia-labelis translated into your page's language. - Reduced motion: under
prefers-reduced-motion: reduce, the three animations are disabled; the color and fill transitions on the heart (grey to red) stay active. The liked state remains clearly readable without any motion. - Touch: the button responds normally to touch events. Its 80 px diameter target is comfortable for a finger (recommended minimum: 44 px).
Browser compatibility
Pure CSS (animations, --angle custom property, pseudo-elements) + 0.5 KB of JavaScript (classList, addEventListener, aria-pressed), no dependency. Verified in Chromium 151: three simultaneous animations, aria-pressed updated, two independent copies on the same page, 0 errors.
Without JavaScript, animations still fire via the .liked class as long as an onclick attribute is present; aria-pressed will not be updated. Without CSS animations, the heart stays grey at rest and red on click (color change only).
The code
Copy the three blocks into your page. No dependencies.
<div class="fx-0361-root">
<div class="mi-heart-scene">
<button class="mi-heart-btn" aria-pressed="false" aria-label="Like">
<svg viewBox="0 0 24 24"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
<div class="mi-heart-particles">
<span class="mi-heart-particle"></span>
<span class="mi-heart-particle"></span>
<span class="mi-heart-particle"></span>
<span class="mi-heart-particle"></span>
<span class="mi-heart-particle"></span>
<span class="mi-heart-particle"></span>
<span class="mi-heart-particle"></span>
<span class="mi-heart-particle"></span>
</div>
</button>
<span class="mi-heart-count">423 likes</span>
</div>
</div>
.fx-0361-root,
.fx-0361-root *,
.fx-0361-root *::before,
.fx-0361-root *::after {
box-sizing: border-box
}
.fx-0361-root {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 250px;
overflow: hidden;
position: relative;
background: rgb(26, 26, 36);
color: rgb(255, 255, 255);
font-family: sans-serif;
}
.mi-heart-scene {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
.mi-heart-btn {
width: 80px;
height: 80px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.05);
border: 2px solid rgba(255, 255, 255, 0.1);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
position: relative;
transition: background-color 0.3s ease, border-color 0.3s ease;
}
.mi-heart-btn:hover {
background: rgba(239, 68, 68, 0.1);
border-color: rgba(239, 68, 68, 0.3);
}
.mi-heart-btn:focus-visible {
outline: 2px solid #a5b4fc;
outline-offset: 3px;
}
.mi-heart-btn svg {
width: 36px;
height: 36px;
stroke: rgba(255, 255, 255, 0.5);
fill: none;
stroke-width: 2;
transition: stroke 0.3s ease, fill 0.3s ease;
}
.mi-heart-btn.liked svg {
stroke: #ef4444;
fill: #ef4444;
animation: miHeartPop 0.4s cubic-bezier(0.17, 0.89, 0.32, 1.49);
}
.mi-heart-btn.liked::after {
content: '';
position: absolute;
inset: -4px;
border-radius: 50%;
border: 2px solid #ef4444;
animation: miHeartRing 0.6s ease-out forwards;
}
.mi-heart-particles {
position: absolute;
inset: 0;
pointer-events: none;
}
.mi-heart-particle {
position: absolute;
width: 6px;
height: 6px;
border-radius: 50%;
top: 50%;
left: 50%;
opacity: 0;
}
.mi-heart-btn.liked .mi-heart-particle {
animation: miParticleBurst 0.6s ease-out forwards;
}
.mi-heart-particle:nth-child(1) { background: #ef4444; --angle: 0deg; }
.mi-heart-particle:nth-child(2) { background: #f97316; --angle: 45deg; }
.mi-heart-particle:nth-child(3) { background: #eab308; --angle: 90deg; }
.mi-heart-particle:nth-child(4) { background: #ef4444; --angle: 135deg; }
.mi-heart-particle:nth-child(5) { background: #ec4899; --angle: 180deg; }
.mi-heart-particle:nth-child(6) { background: #f97316; --angle: 225deg; }
.mi-heart-particle:nth-child(7) { background: #eab308; --angle: 270deg; }
.mi-heart-particle:nth-child(8) { background: #ec4899; --angle: 315deg; }
.mi-heart-count {
font-size: 0.85rem;
font-weight: 600;
color: rgba(255, 255, 255, 0.5);
transition: color 0.3s ease;
}
.mi-heart-btn.liked ~ .mi-heart-count {
color: #ef4444;
}
@keyframes miHeartPop {
0% { transform: scale(1); }
30% { transform: scale(1.3); }
60% { transform: scale(0.9); }
100% { transform: scale(1); }
}
@keyframes miHeartRing {
0% { transform: scale(0.8); opacity: 1; }
100% { transform: scale(1.4); opacity: 0; }
}
@keyframes miParticleBurst {
0% { opacity: 1; transform: translate(-50%, -50%) rotate(var(--angle)) translateY(0); }
100% { opacity: 0; transform: translate(-50%, -50%) rotate(var(--angle)) translateY(-40px); }
}
@media (prefers-reduced-motion: reduce) {
.mi-heart-btn { transition: background-color 0.15s, border-color 0.15s; }
.mi-heart-btn svg { transition: stroke 0.15s, fill 0.15s; }
.mi-heart-btn.liked svg { animation: none; }
.mi-heart-btn.liked::after { animation: none; opacity: 0; }
.mi-heart-btn.liked .mi-heart-particle { animation: none; }
}
!function(){document.querySelectorAll('.fx-0361-root').forEach(function(root){var btn=root.querySelector('.mi-heart-btn');if(!btn)return;btn.addEventListener('click',function(){var liked=btn.classList.toggle('liked');btn.setAttribute('aria-pressed',liked?'true':'false');});})}();
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Heart color (CSS — stroke and fill on .mi-heart-btn.liked svg) |
#ef4444 (red) |
Switch to #ec4899 for fuchsia pink or #8b5cf6 for violet. Update the ring (::after border) and counter (.mi-heart-btn.liked ~ .mi-heart-count) to the same value. |
Number of particles (HTML — number of span.mi-heart-particle elements) |
8 particles, every 45° | Reduce to 6 by removing 2 spans and setting --angle to 60°, 120°…, or raise to 12 at 30° intervals. Each new particle needs its --angle declared in CSS. |
Particle travel distance (CSS — translateY(-40px) in miParticleBurst) |
40 px | 60 px gives a wider burst; 24 px a short burst that stays close to the button. |
Button size (CSS — width / height of .mi-heart-btn) |
80 px diameter | Drop to 56 px for a dense list; raise to 100 px for a standalone use. Particle and SVG sizes follow if expressed in em. |
| Icon (HTML — SVG inside the button) | Heart SVG | Replace the SVG with a star, a flame or any other 24 px SVG icon. Adjust the miHeartPop rotation if the icon is asymmetric. |
Counter (HTML — span.mi-heart-count) |
“423 likes” | Update the number via JavaScript (count.textContent = ...) in your app logic. The color animation on the counter is pure CSS via the ~ sibling selector. |
FAQ
Does the button go back to its initial state if clicked again?
Yes. The script uses classList.toggle('liked'): a second click removes the class, the animations do not replay, colors return to their default via CSS transition, and aria-pressed goes back to “false”.
Can several heart buttons coexist on the same page?
Yes, without restriction. The script finds every .fx-0361-root on the page and wires each button independently. Each copy has its own state: liking one does not affect the others.
Do the animations replay if you hover over the button after liking it?
No. The CSS animations are declared without animation-iteration-count: infinite: they play once when the class is added. A second hover without removing the class does not restart them.