syndicated · dev.to / @markyu
A cleaner CSS heart animation tutorial focused on transform, pseudo-elements, keyframes, and the small mistakes that break simple UI animations.
- Published
- May 22 '24
- Reading time
- 2 min read
- Reactions
- 9
Small CSS animations are underrated.
They look like toy demos, but they teach the same things you need for real UI work: transforms, timing, layout, pseudo-elements, and not accidentally triggering expensive repaint work.
Let’s build a heart animation with mostly CSS.
The HTML
Keep the markup boring:
<main class="stage">
<button class="heart-button" aria-label="Like">
<span class="heart"></span>
</button>
</main>The button gives us keyboard accessibility. The span handles the shape.
The CSS Shape
A heart is basically a rotated square with two circles attached.
.stage {
min-height: 100vh;
display: grid;
place-items: center;
background: #111827;
}
.heart-button {
border: 0;
background: transparent;
cursor: pointer;
padding: 48px;
}
.heart {
position: relative;
display: block;
width: 80px;
height: 80px;
background: #ef4444;
transform: rotate(45deg);
}
.heart::before,
.heart::after {
content: "";
position: absolute;
width: 80px;
height: 80px;
border-radius: 50%;
background: #ef4444;
}
.heart::before {
left: -40px;
}
.heart::after {
top: -40px;
}Visual model:
circle + circle
\ /
rotated squareThat is the whole trick.
Add the Pulse
Use transform, not layout properties.
.heart-button:hover .heart,
.heart-button:focus-visible .heart {
animation: pulse 700ms ease-in-out infinite;
}
@keyframes pulse {
0% {
transform: rotate(45deg) scale(1);
}
45% {
transform: rotate(45deg) scale(1.18);
}
100% {
transform: rotate(45deg) scale(1);
}
}The common mistake is forgetting the rotation inside the keyframes. If you animate only scale(), the heart can snap back because transform gets replaced.
Bad:
transform: scale(1.2);Good:
transform: rotate(45deg) scale(1.2);Add a Click State With JavaScript
CSS hover is nice, but click feedback feels better.
const button = document.querySelector(".heart-button");
button.addEventListener("click", () => {
button.classList.toggle("is-liked");
});.heart-button.is-liked .heart,
.heart-button.is-liked .heart::before,
.heart-button.is-liked .heart::after {
background: #fb7185;
}
.heart-button.is-liked .heart {
box-shadow: 0 0 48px rgba(251, 113, 133, 0.55);
}Respect Reduced Motion
This is a small demo, but accessibility still counts.
@media (prefers-reduced-motion: reduce) {
.heart-button:hover .heart,
.heart-button:focus-visible .heart {
animation: none;
}
}I would add this even for playful UI. Not every user wants pulsing motion.
What This Demo Teaches
| Technique | Real UI use |
|---|---|
::before / ::after | icons, badges, decorative layers |
transform | cheap animation |
@keyframes | reusable motion |
focus-visible | keyboard-friendly interaction |
| reduced motion | accessibility polish |
Final Thought
This is not just a heart animation. It is a tiny lab for learning how browser motion behaves.
What small CSS animation taught you more than you expected?
Related reading
CSS 3D Transform Bugs Usually Come From Perspective
A practical CSS 3D transform guide explaining perspective, rotateX, rotateY, transform-style, backface visibility, and debugging layout.
css
React Loading Screens Are a State Machine Problem
A practical React loading screen guide using hooks, request states, error states, CSS animation, and why spinners alone are not enough.
react
Frontend Linear Data Structures Deep Dive: Arrays, Stacks, Queues, and Linked Lists
The Big Picture Before diving into stacks, queues, and linked lists, it helps to know...
computerscience
originally published
This post first ran on dev.to. Comments and reactions live there.
Continue on dev.to