Add sparkles to heart on click
On Car & Classic we have a couple places where we allow a user to save a resource to their account (an advert, a search, an auction) and for that we have hearts.
When you click on a heart to save, we display a feedback to the user letting him know his resource was saved but, I thought this deserved a bit more love and could have a much better experience to the user.
This is our current feedback to the user whenever he saves a resource:
The idea was to convert our heart animation to something more playful that has a couple sparkles coming out of the heart once it gets clicked:

Ferrari Monza SP1 2020 for sale
4297cc · Petrol · 38,000 miles · Automatic · 7+ speed · RHD
€63,800
In order to create such a heart effect we will need to break it down into the following steps:
- Make the heart go from fill transparent to a red fill colour
- Once a user clicks, we will need “sparks” to go out on random directions
- Make the sparks disappear once they reach their final destination
Changing the heart’s fill color
The idea is simple: when the user clicks in the heart, it should transition the color from transparent into a red filled heart
<div class="wrapper">
<button class="btn">
<svg viewBox="0 0 24 24">
<path
stroke="currentColor"
stroke-width="1.5"
d="M12 8c-1.574-1.635-4.46-2.135-6.035-.5c-1.573 1.635-1.34 3.836 0 5.752S9.41 16.89 12 19c2.59-2.11 4.694-3.832 6.035-5.748c1.34-1.916 1.573-4.117 0-5.752C16.46 5.865 13.574 6.365 12 8Z"
/>
</svg>
</button>
</div>The code above does two things:
- Attach a listener to the button
- When the button gets clicked, it attaches a
data-activeattribute to the button - The
data-activeattribute allows us to style the button differently (and later position the sparks)
Adding the sparkles
Now that we have a heart that changes color when a user clicks on it, we can add our sparkles. We’ll do it in three steps: place them, move them, then fade them out.
Placing the sparkles
First, we add a few <span class="sparkle"> elements inside the button. Each sparkle is a small dot with
position: absolute, placed at the center of the button with top: 50% and left: 50%.
top and left move the top-left corner of the sparkle to the center, not the sparkle itself. That’s why we
also add translate(-50%, -50%), which pulls the sparkle back by half its own size so it sits right in the middle.
For now, all sparkles sit in the same spot, on top of each other:
<div class="wrapper">
<button class="btn">
<svg viewBox="0 0 24 24">
<path
stroke="currentColor"
stroke-width="1.5"
d="M12 8c-1.574-1.635-4.46-2.135-6.035-.5c-1.573 1.635-1.34 3.836 0 5.752S9.41 16.89 12 19c2.59-2.11 4.694-3.832 6.035-5.748c1.34-1.916 1.573-4.117 0-5.752C16.46 5.865 13.574 6.365 12 8Z"
/>
</svg>
<span class="sparkle"></span>
<span class="sparkle"></span>
<span class="sparkle"></span>
<span class="sparkle"></span>
<span class="sparkle"></span>
</button>
</div>Note that the button needs position: relative, so the sparkles are placed based on the button and not the page.
Moving the sparkles
When the button is clicked, each sparkle should move out to its own spot around the heart.
To spread them evenly, we split the circle into equal slices. A full circle is Math.PI * 2, so each slice is
(Math.PI * 2) / sparkles.length. With 5 sparkles, that’s one sparkle every 72°.
Heads up!
Heads up!
There is a How to draw a circle article that dives a bit deeper on the math behind the Javascript in this article.
To make it look less rigid, we add a small random jitter to each angle. The jitter stays under half a slice,
so two sparkles never end up on top of each other.
With the angle and a distance, Math.cos and Math.sin give us the x and y of each sparkle.
We save them as the CSS variables --x and --y, and use them in the CSS when the button has data-active.
The transition on transform makes the sparkles slide out instead of jumping:
<div class="wrapper">
<button class="btn">
<svg viewBox="0 0 24 24">
<path
stroke="currentColor"
stroke-width="1.5"
d="M12 8c-1.574-1.635-4.46-2.135-6.035-.5c-1.573 1.635-1.34 3.836 0 5.752S9.41 16.89 12 19c2.59-2.11 4.694-3.832 6.035-5.748c1.34-1.916 1.573-4.117 0-5.752C16.46 5.865 13.574 6.365 12 8Z"
/>
</svg>
<span class="sparkle"></span>
<span class="sparkle"></span>
<span class="sparkle"></span>
<span class="sparkle"></span>
<span class="sparkle"></span>
</button>
</div>We keep translate(-50%, -50%) in front of the new translate. Without it, the sparkles would move from
their top-left corner and not from the center of the button.
When you click the button again, data-active is removed and the sparkles slide back to the center.
Fading the sparkles out
The sparkles move, but they stay on screen. Two things are left:
- Hide the sparkles until the button is clicked
- Fade them out once they reach their spot
We set opacity: 0 on the sparkles by default, and opacity: 1 when the button has data-active.
Then we add a sparkle-fade animation that takes the opacity back to 0.
forwards keeps the sparkles at opacity: 0 when the animation ends, so they don’t show up again.
When the button is turned off, the animation is removed and the sparkles slide back to the center while hidden.
The next click plays the animation again from the start:
<div class="wrapper">
<button class="btn">
<svg viewBox="0 0 24 24">
<path
stroke="currentColor"
stroke-width="1.5"
d="M12 8c-1.574-1.635-4.46-2.135-6.035-.5c-1.573 1.635-1.34 3.836 0 5.752S9.41 16.89 12 19c2.59-2.11 4.694-3.832 6.035-5.748c1.34-1.916 1.573-4.117 0-5.752C16.46 5.865 13.574 6.365 12 8Z"
/>
</svg>
<span class="sparkle"></span>
<span class="sparkle"></span>
<span class="sparkle"></span>
<span class="sparkle"></span>
<span class="sparkle"></span>
</button>
</div>The fade lasts twice as long as the move (calc(var(--sparkle-fade)*2)), so the sparkles are still visible
while they slide out and disappear once they get there.
Accessibility
To keep this post focused on the animation, I left accessibility out of the examples. The heart works with a mouse, but it’s not ready for everyone yet. Here is what would fix it:
- The button only has an icon, so a screen reader has nothing to read. Adding an
aria-labellike “Save to favourites” tells the user what the button does. - Toggling
data-activeonly changes the styles. Addingaria-pressedand switching it betweentrueandfalseon click lets screen readers announce the current state. - The SVG and the sparkles are only there to look nice. Adding
aria-hidden="true"to them keeps screen readers from reading them out. - Some users turn off animations in their system settings. With the
prefers-reduced-motionmedia query, we can skip the sparkles and only change the heart’s color.