Max Schmitt

February 18 2022

React: Making a Juicy SVG-Powered Like Animation

In this post I'm going to show you how to create a juicy like animation using React and SVG.

It's heavily inspired by Twitter's like animation but slightly altered and instead of a heart we'll be using a star.

Let's break this animation down – component by component and step by step:

Basic Structure of the Star Button

There are 3 main elements to the star animation. They are layered on top of each other.

The layers of the star button visualized

Component 1: <StarButton>

The button that makes the whole thing interactive and clickable. It contains both the <StarAnimation> and the <StarIcon>.

The star button visualized

StarButton.js

function StarButton() {
const [starred, setStarred] = useState(false)
const toggleStarred = () => {
setStarred((starred) => !starred)
}
return (
<button className={`StarButton ${starred ? 'StarButton--starred' : ''}`} type="button" onClick={toggleStarred}>
<StarAnimation starred={starred} />
<StarIcon />
</button>
)
}

Component 2: <StarIcon>

This is just the star icon. This component will remain unchanged and we'll later add some styling via CSS.

The star icon visualized

StarIcon.js

function StarIcon() {
return (
<svg className="StarIcon" width={32} height={32} fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 2l3.95 8.563 9.365 1.11-6.924 6.404 1.838 9.25L16 22.72l-8.229 4.606 1.838-9.25-6.924-6.402 9.365-1.11L16 2z" />
</svg>
)
}

Component 3: <StarAnimation>

This is a blank SVG for now that we'll begin filling in in the next step.

The star animation visualized

StarAnimation.js

function StarAnimation({ starred }) {
return (
<svg className="StarAnimation" width={96} height={96} fill="none" xmlns="http://www.w3.org/2000/svg">
{/* ... */}
</svg>
)
}

Basic Styles

Before we begin filling in our <StarAnimation>, let's first add some basic styles to position our components.

The anatomy of the star button visualized

StarButton.css

.StarButton {
line-height: 0;
padding: 0;
background-color: transparent;
border: 0;
position: relative;
cursor: pointer;
}
.StarButton .StarAnimation {
position: absolute;
top: -32px;
left: -32px;
pointer-events: none;
}
.StarButton .StarIcon {
position: relative;
}

With that little bit of boilerplate out of the way, let's get on to the good stuff!

SVG Animation 1: The Pulse Wave

When the button is clicked, you can see a pulse wave appearing from the center.

Let's add this effect to our <StarAnimation> component.

We're animating an SVG circle with a stroke (border). Using a CSS transition, we will animate the circle from transform: scale(0); opacity: 1; to transform: scale(1); opacity: 0;.

JS

function PulseWave({ starred = false }) {
const initialStyle = {
transform: 'scale(0)',
opacity: 1,
}
const style = starred
? {
transform: 'scale(1)',
opacity: 0,
transformOrigin: 'center center',
transition: 'transform 400ms, opacity 400ms',
transitionDelay: '0ms, 200ms',
}
: initialStyle
return (
<circle
// Position the circle in the center of the 96x96 SVG
cx={48}
cy={48}
// This radius makes the circle go all the way to the edge of the SVG.
// We subtract 1px from 48px to account for the strokeWidth of 1px.
r={47}
strokeWidth={1}
stroke="#f7d527"
style={style}
/>
)
}

Now let's add this component to our <StarAnimation> component:

StarAnimation.js

function StarAnimation({ starred }) {
return (
<svg className="StarAnimation" width={96} height={96} fill="none" xmlns="http://www.w3.org/2000/svg">
<PulseWave starred={starred} />
</svg>
)
}

SVG Animation 2: The Particles

Perhaps more prominent than the pulse wave, the particles appear when clicking the star button.

To create these particles, we can follow a few simple steps:

1. Create a number of filled SVG circles in the center of the SVG

JS

function Particles({ starred }) {
return Array.from({ length: numParticles }).map((_, i) => {
// Position the cirlce at the center of the SVG
const cx = centerX
const cy = centerY
// Give the circle a random fill
const yellows = ['#F7D527', '#E0C016', '#C1A40A']
const fill = yellows[Math.floor(Math.random() * yellows.length)]
// Give the circle a random size
const minParticleRadius = 1
const maxParticleRadius = 3
const radius = minParticleRadius + Math.random() * (maxParticleRadius - minParticleRadius)
return <circle key={i} cx={cx} cy={cy} r={radius} fill={fill} />
})
}

2. Calculate the target position of each circle

JS

function Particles({ starred }) {
Array.from({ length: numParticles }).map((_, i) => {
// Calculate cx, cy, fill, radius
// ...
// Determine a random length that the circle will travel
const minParticleDistance = 32
const maxParticleDistance = 42
const travelDistance =
minParticleDistance + Math.random() * (maxParticleDistance - minParticleDistance) - radius
// The angle is dependent on the particle's index. We want the
// particles to evenly travel outwards in a circle shape.
// The angle for the particle is 360deg * (i / numParticles)
const travelAngle = 2 * Math.PI * (i / numParticles)
// Once we know the travel distance and angle, we can determine the
// target position of the particle
const targetCx = centerX + Math.cos(travelAngle) * travelDistance
const targetCy = centerY + Math.sin(travelAngle) * travelDistance
return <circle key={i} cx={cx} cy={cy} r={radius} fill={fill} />
})
}

3. Apply the animation using CSS transforms

JS

function Particles({ starred }) {
Array.from({ length: numParticles }).map((_, i) => {
// Calculate cx, cy, fill, radius
// ...
// Calculate targetCx, targetCy
// ...
// The amount we need to move the particle by is simply
// targetPosition - originalPosition
const translateX = targetCx - cx
const translateY = targetCy - cy
// Get random durations for movement and opacity
// Travel duration will be between 200ms and 500ms
const travelDuration = 200 + Math.random() * 300
// Opacity duration will be slightly longer than travel duration
const opacityDuration = travelDuration * 1.2
// Each particle starts out at the center with opacity 1. It then
// animates to its target destination with opacity 0
const initialStyle = {
transform: `translate(0px, 0px)`,
opacity: 1,
}
const style = starred
? {
transform: `translate(${translateX}px, ${translateY}px)`,
opacity: 0,
transition: `transform ${travelDuration}ms, opacity ${opacityDuration}ms`,
transitionDelay: `0ms, ${travelDuration * 0.8}ms`,
}
: initialStyle
return <circle key={i} cx={cx} cy={cy} r={radius} fill={fill} style={style} />
})
}

As a final step, render the <Particles> component inside the <StarAnimation> component.

Animating the Star

Lastly, let's look at how we can animate the star itself to look nice and juicy.

The animation here can be accomplished entirely with CSS. There's a few things going on.

  1. Give the star a gray stroke by default
  2. When the user starts clicking/touching, make the star slightly smaller
  3. When starred, add a yellow fill to the star and remove the stroke
  4. When starred, make the star larger
  5. When starred, rotate the star by 2 spikes

And here are these steps implemented with CSS:

StarButton.css

.StarButton .StarIcon {
/* 1. Give the star a gray `stroke` by default */
stroke: #ababab;
/* We'll be animating the transform of the StarIcon */
transition: transform 100ms;
}
.StarButton:active .StarIcon {
/* 2. When the user starts clicking/touching, make the star
slightly smaller */
transform: scale(0.7);
}
.StarButton--starred .StarIcon {
/* 3. When `starred`, add a yellow `fill` to the star and
remove the `stroke` */
fill: #f7d527;
stroke: none;
/* 4. When `starred`, make the star larger */
/* 5. When `starred`, rotate the star by 2 spikes */
animation: 650ms star-grow-rotate ease-out;
}
@keyframes star-grow-rotate {
0% {
transform: scale(1) rotate(0deg);
}
50% {
transform: scale(1.5) rotate(120deg);
}
100% {
/* The star has 5 spikes: 360deg / 5 = 72deg.
-> 72deg is a rotation by 1 spike.
-> 144deg is a rotation by 2 spikes.
*/
transform: scale(1) rotate(144deg);
}
}

That was it! If you followed along up until here, you should now have a working juicy star animation.

You can find an entire working example on CodeSandbox.