UI Components Easy
Animated Like Button
A micro-interaction focused like button with smooth heart animations and popping effects.
Open in Lab
MCP
vanilla-js css react tailwind svelte vue
Targets: TS JS HTML React React Native Svelte Vue
Expo Snack
Code
body {
margin: 0;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
:root {
--heart-default: rgba(255, 255, 255, 0.25);
--heart-active: #f43f5e;
--heart-glow: rgba(244, 63, 94, 0.35);
--count-color: #94a3b8;
--count-active: #f8fafc;
}
.like-wrapper {
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-family: "Inter", system-ui, sans-serif;
padding: 0.5rem;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 50px;
backdrop-filter: blur(8px);
transition: border-color 0.3s;
}
.like-wrapper:has(.like-btn.liked) {
border-color: rgba(244, 63, 94, 0.3);
background: rgba(244, 63, 94, 0.06);
}
.like-btn {
background: none;
border: none;
cursor: pointer;
position: relative;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
transition: transform 0.2s;
outline: none;
border-radius: 50%;
}
.like-btn:hover {
background: rgba(244, 63, 94, 0.1);
}
.like-btn:active {
transform: scale(0.88);
}
.heart-icon {
width: 24px;
height: 24px;
fill: none;
stroke: var(--heart-default);
stroke-width: 2;
transition: all 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.like-btn.liked .heart-icon {
fill: var(--heart-active);
stroke: var(--heart-active);
animation: heartPop 0.45s ease;
filter: drop-shadow(0 0 8px var(--heart-glow));
}
.like-count {
font-size: 0.938rem;
font-weight: 700;
color: var(--count-color);
min-width: 2ch;
padding-right: 0.5rem;
letter-spacing: -0.01em;
transition: color 0.3s;
}
.like-btn.liked ~ .like-count {
color: var(--count-active);
}
@keyframes heartPop {
0% {
transform: scale(1);
}
40% {
transform: scale(1.5);
}
70% {
transform: scale(0.9);
}
100% {
transform: scale(1);
}
}
/* Particles */
.particles {
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 100%;
pointer-events: none;
}
.particles span {
position: absolute;
display: block;
width: 5px;
height: 5px;
background: var(--heart-active);
border-radius: 50%;
opacity: 0;
}
.like-btn.liked .particles span {
animation: explode 0.6s ease forwards;
}
@keyframes explode {
0% {
transform: translate(-50%, -50%) scale(0);
opacity: 1;
}
100% {
transform: translate(
calc(cos(calc(var(--i) * 60deg)) * 28px),
calc(sin(calc(var(--i) * 60deg)) * 28px)
)
scale(1.5);
opacity: 0;
}
}const likeBtn = document.getElementById("like-btn");
const likeCountEl = document.getElementById("like-count");
let count = 1234;
let liked = false;
likeBtn.addEventListener("click", () => {
liked = !liked;
if (liked) {
likeBtn.classList.add("liked");
count++;
} else {
likeBtn.classList.remove("liked");
count--;
}
likeCountEl.textContent = count.toLocaleString();
});<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Like Button</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="like-wrapper">
<button id="like-btn" class="like-btn" aria-label="Like">
<svg class="heart-icon" viewBox="0 0 24 24">
<path
d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" />
</svg>
<div class="particles">
<span style="--i:1"></span>
<span style="--i:2"></span>
<span style="--i:3"></span>
<span style="--i:4"></span>
<span style="--i:5"></span>
<span style="--i:6"></span>
</div>
</button>
<span id="like-count" class="like-count">1,234</span>
</div>
<script src="script.js"></script>
</body>
</html>import { useState } from "react";
export default function LikeButtonRC() {
const [liked, setLiked] = useState(false);
const [count, setCount] = useState(142);
const [burst, setBurst] = useState(false);
function toggle() {
if (!liked) {
setBurst(true);
setTimeout(() => setBurst(false), 600);
}
setLiked((l) => !l);
setCount((c) => (liked ? c - 1 : c + 1));
}
const PARTICLES = Array.from({ length: 6 }, (_, i) => ({
angle: (i / 6) * 360,
color: ["#ff6b6b", "#ff8e53", "#ff6b9d", "#c56cf0", "#ff9ff3", "#ffd32a"][i],
}));
return (
<div className="min-h-screen bg-[#0d1117] flex flex-col items-center justify-center gap-8 p-6">
{/* Single like button */}
<div className="relative flex flex-col items-center">
<button
onClick={toggle}
aria-label={liked ? "Unlike" : "Like"}
className="relative flex items-center gap-2.5 px-6 py-3 rounded-full border transition-all duration-200 select-none"
style={{
background: liked ? "rgba(255,107,107,0.12)" : "rgba(255,255,255,0.04)",
borderColor: liked ? "rgba(255,107,107,0.4)" : "rgba(255,255,255,0.1)",
transform: burst ? "scale(0.93)" : "scale(1)",
}}
>
<svg
width="22"
height="22"
viewBox="0 0 24 24"
fill={liked ? "#ff6b6b" : "none"}
stroke={liked ? "#ff6b6b" : "#8b949e"}
strokeWidth="2"
style={{
transition: "all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1)",
transform: liked ? "scale(1.2)" : "scale(1)",
}}
>
<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>
<span
className="font-semibold text-sm tabular-nums transition-colors"
style={{ color: liked ? "#ff6b6b" : "#8b949e" }}
>
{count.toLocaleString()}
</span>
</button>
{/* Burst particles */}
{burst &&
PARTICLES.map((p, i) => (
<span
key={i}
className="absolute w-2 h-2 rounded-full pointer-events-none"
style={
{
background: p.color,
animation: "particle-burst 0.6s ease-out both",
transformOrigin: "center",
"--angle": `${p.angle}deg`,
} as React.CSSProperties
}
/>
))}
</div>
{/* Variants showcase */}
<div className="grid grid-cols-3 gap-4">
{[
{ icon: "♥", label: "Love", color: "#ff6b6b", count: 248 },
{ icon: "👍", label: "Like", color: "#58a6ff", count: 1024 },
{ icon: "★", label: "Star", color: "#f1e05a", count: 87 },
].map(({ icon, label, color, count: c }) => {
return (
<LikeVariant key={label} icon={icon} label={label} color={color} initialCount={c} />
);
})}
</div>
<style>{`
@keyframes particle-burst {
0% { transform: translate(0,0) scale(1); opacity: 1; }
100% { transform: translate(calc(cos(var(--angle)) * 32px), calc(sin(var(--angle)) * 32px)) scale(0); opacity: 0; }
}
`}</style>
</div>
);
}
function LikeVariant({
icon,
label,
color,
initialCount,
}: { icon: string; label: string; color: string; initialCount: number }) {
const [active, setActive] = useState(false);
const [count, setCount] = useState(initialCount);
function toggle() {
setActive((a) => !a);
setCount((c) => (active ? c - 1 : c + 1));
}
return (
<button
onClick={toggle}
className="flex flex-col items-center gap-1.5 py-3 px-4 rounded-xl border transition-all duration-200"
style={{
background: active ? `${color}18` : "rgba(255,255,255,0.03)",
borderColor: active ? `${color}40` : "rgba(255,255,255,0.08)",
transform: active ? "scale(1.05)" : "scale(1)",
}}
>
<span
className="text-2xl"
style={{ filter: active ? "none" : "grayscale(1) opacity(0.4)", transition: "filter 0.2s" }}
>
{icon}
</span>
<span
className="text-xs font-semibold tabular-nums"
style={{ color: active ? color : "#8b949e" }}
>
{count.toLocaleString()}
</span>
<span className="text-[10px] text-[#484f58]">{label}</span>
</button>
);
}import React, { useState, useRef } from "react";
import { View, Text, TouchableOpacity, Animated, StyleSheet } from "react-native";
interface LikeButtonProps {
liked: boolean;
onToggle: () => void;
size?: number;
}
const PARTICLE_COUNT = 6;
function LikeButton({ liked, onToggle, size = 48 }: LikeButtonProps) {
const scale = useRef(new Animated.Value(1)).current;
const particles = useRef(
Array.from({ length: PARTICLE_COUNT }, () => ({
opacity: new Animated.Value(0),
translateX: new Animated.Value(0),
translateY: new Animated.Value(0),
scale: new Animated.Value(0),
}))
).current;
const handlePress = () => {
if (!liked) {
// Scale spring
Animated.sequence([
Animated.spring(scale, {
toValue: 1.3,
useNativeDriver: true,
tension: 300,
friction: 6,
}),
Animated.spring(scale, {
toValue: 1,
useNativeDriver: true,
tension: 200,
friction: 8,
}),
]).start();
// Particle burst
const angles = Array.from(
{ length: PARTICLE_COUNT },
(_, i) => (i * 2 * Math.PI) / PARTICLE_COUNT
);
particles.forEach((p, i) => {
const angle = angles[i];
const dist = size * 0.8;
p.opacity.setValue(1);
p.translateX.setValue(0);
p.translateY.setValue(0);
p.scale.setValue(1);
Animated.parallel([
Animated.timing(p.translateX, {
toValue: Math.cos(angle) * dist,
duration: 500,
useNativeDriver: true,
}),
Animated.timing(p.translateY, {
toValue: Math.sin(angle) * dist,
duration: 500,
useNativeDriver: true,
}),
Animated.timing(p.opacity, {
toValue: 0,
duration: 500,
useNativeDriver: true,
}),
Animated.timing(p.scale, {
toValue: 0,
duration: 500,
useNativeDriver: true,
}),
]).start();
});
} else {
// Unlike: gentle scale
Animated.sequence([
Animated.timing(scale, {
toValue: 0.85,
duration: 100,
useNativeDriver: true,
}),
Animated.spring(scale, {
toValue: 1,
useNativeDriver: true,
tension: 200,
friction: 10,
}),
]).start();
}
onToggle();
};
const heartSize = size * 0.6;
const particleSize = size * 0.2;
return (
<View
style={{ width: size * 2, height: size * 2, alignItems: "center", justifyContent: "center" }}
>
{/* Particles */}
{particles.map((p, i) => (
<Animated.View
key={i}
style={{
position: "absolute",
opacity: p.opacity,
transform: [
{ translateX: p.translateX },
{ translateY: p.translateY },
{ scale: p.scale },
],
}}
>
<Text style={{ fontSize: particleSize }}>❤️</Text>
</Animated.View>
))}
<TouchableOpacity onPress={handlePress} activeOpacity={0.8}>
<Animated.View style={{ transform: [{ scale }] }}>
<Text style={{ fontSize: heartSize, color: liked ? "#ef4444" : "#64748b" }}>
{liked ? "❤️" : "🤍"}
</Text>
</Animated.View>
</TouchableOpacity>
</View>
);
}
// --- Demo ---
export default function App() {
const [liked1, setLiked1] = useState(false);
const [liked2, setLiked2] = useState(true);
const [liked3, setLiked3] = useState(false);
const [count1, setCount1] = useState(42);
const [count2, setCount2] = useState(128);
const [count3, setCount3] = useState(7);
return (
<View style={styles.container}>
<Text style={styles.title}>Like Button</Text>
<View style={styles.row}>
<View style={styles.likeColumn}>
<LikeButton
liked={liked1}
onToggle={() => {
setLiked1(!liked1);
setCount1((c) => (liked1 ? c - 1 : c + 1));
}}
size={36}
/>
<Text style={styles.count}>{count1}</Text>
<Text style={styles.sizeLabel}>Small</Text>
</View>
<View style={styles.likeColumn}>
<LikeButton
liked={liked2}
onToggle={() => {
setLiked2(!liked2);
setCount2((c) => (liked2 ? c - 1 : c + 1));
}}
size={48}
/>
<Text style={styles.count}>{count2}</Text>
<Text style={styles.sizeLabel}>Medium</Text>
</View>
<View style={styles.likeColumn}>
<LikeButton
liked={liked3}
onToggle={() => {
setLiked3(!liked3);
setCount3((c) => (liked3 ? c - 1 : c + 1));
}}
size={64}
/>
<Text style={styles.count}>{count3}</Text>
<Text style={styles.sizeLabel}>Large</Text>
</View>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#0f172a",
alignItems: "center",
justifyContent: "center",
padding: 24,
},
title: {
color: "#f8fafc",
fontSize: 24,
fontWeight: "700",
marginBottom: 40,
},
row: {
flexDirection: "row",
alignItems: "flex-end",
gap: 16,
},
likeColumn: {
alignItems: "center",
},
count: {
color: "#94a3b8",
fontSize: 16,
fontWeight: "600",
marginTop: 4,
},
sizeLabel: {
color: "#475569",
fontSize: 12,
marginTop: 4,
},
});<script>
let liked = false;
let count = 142;
let burst = false;
const PARTICLES = Array.from({ length: 6 }, (_, i) => ({
angle: (i / 6) * 360,
color: ["#ff6b6b", "#ff8e53", "#ff6b9d", "#c56cf0", "#ff9ff3", "#ffd32a"][i],
}));
const variants = [
{ icon: "\u2665", label: "Love", color: "#ff6b6b", initialCount: 248 },
{ icon: "\uD83D\uDC4D", label: "Like", color: "#58a6ff", initialCount: 1024 },
{ icon: "\u2605", label: "Star", color: "#f1e05a", initialCount: 87 },
];
let variantStates = variants.map((v) => ({
active: false,
count: v.initialCount,
}));
function toggle() {
if (!liked) {
burst = true;
setTimeout(() => (burst = false), 600);
}
liked = !liked;
count = liked ? count + 1 : count - 1;
}
function toggleVariant(i) {
const s = variantStates[i];
s.active = !s.active;
s.count = s.active ? s.count + 1 : s.count - 1;
variantStates = variantStates;
}
</script>
<div class="min-h-screen bg-[#0d1117] flex flex-col items-center justify-center gap-8 p-6">
<!-- Single like button -->
<div class="relative flex flex-col items-center">
<button
on:click={toggle}
aria-label={liked ? "Unlike" : "Like"}
class="relative flex items-center gap-2.5 px-6 py-3 rounded-full border transition-all duration-200 select-none"
style="background: {liked ? 'rgba(255,107,107,0.12)' : 'rgba(255,255,255,0.04)'}; border-color: {liked ? 'rgba(255,107,107,0.4)' : 'rgba(255,255,255,0.1)'}; transform: {burst ? 'scale(0.93)' : 'scale(1)'};"
>
<svg
width="22" height="22" viewBox="0 0 24 24"
fill={liked ? "#ff6b6b" : "none"}
stroke={liked ? "#ff6b6b" : "#8b949e"}
stroke-width="2"
style="transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1); transform: {liked ? 'scale(1.2)' : 'scale(1)'};"
>
<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>
<span
class="font-semibold text-sm tabular-nums transition-colors"
style="color: {liked ? '#ff6b6b' : '#8b949e'};"
>
{count.toLocaleString()}
</span>
</button>
<!-- Burst particles -->
{#if burst}
{#each PARTICLES as p, i}
<span
class="absolute w-2 h-2 rounded-full pointer-events-none"
style="background: {p.color}; animation: particle-burst 0.6s ease-out both; --angle: {p.angle}deg;"
></span>
{/each}
{/if}
</div>
<!-- Variants showcase -->
<div class="grid grid-cols-3 gap-4">
{#each variants as v, i}
<button
on:click={() => toggleVariant(i)}
class="flex flex-col items-center gap-1.5 py-3 px-4 rounded-xl border transition-all duration-200"
style="background: {variantStates[i].active ? v.color + '18' : 'rgba(255,255,255,0.03)'}; border-color: {variantStates[i].active ? v.color + '40' : 'rgba(255,255,255,0.08)'}; transform: {variantStates[i].active ? 'scale(1.05)' : 'scale(1)'};"
>
<span
class="text-2xl"
style="filter: {variantStates[i].active ? 'none' : 'grayscale(1) opacity(0.4)'}; transition: filter 0.2s;"
>
{v.icon}
</span>
<span
class="text-xs font-semibold tabular-nums"
style="color: {variantStates[i].active ? v.color : '#8b949e'};"
>
{variantStates[i].count.toLocaleString()}
</span>
<span class="text-[10px] text-[#484f58]">{v.label}</span>
</button>
{/each}
</div>
</div>
<style>
@keyframes particle-burst {
0% { transform: translate(0,0) scale(1); opacity: 1; }
100% { transform: translate(calc(cos(var(--angle)) * 32px), calc(sin(var(--angle)) * 32px)) scale(0); opacity: 0; }
}
</style><script setup>
import { ref, reactive } from "vue";
const liked = ref(false);
const count = ref(142);
const burst = ref(false);
const PARTICLES = Array.from({ length: 6 }, (_, i) => ({
angle: (i / 6) * 360,
color: ["#ff6b6b", "#ff8e53", "#ff6b9d", "#c56cf0", "#ff9ff3", "#ffd32a"][i],
}));
const variants = [
{ icon: "\u2665", label: "Love", color: "#ff6b6b", initialCount: 248 },
{ icon: "\uD83D\uDC4D", label: "Like", color: "#58a6ff", initialCount: 1024 },
{ icon: "\u2605", label: "Star", color: "#f1e05a", initialCount: 87 },
];
const variantStates = reactive(variants.map((v) => ({ active: false, count: v.initialCount })));
function toggle() {
if (!liked.value) {
burst.value = true;
setTimeout(() => (burst.value = false), 600);
}
liked.value = !liked.value;
count.value = liked.value ? count.value + 1 : count.value - 1;
}
function toggleVariant(i) {
variantStates[i].active = !variantStates[i].active;
variantStates[i].count = variantStates[i].active
? variantStates[i].count + 1
: variantStates[i].count - 1;
}
</script>
<template>
<div class="min-h-screen bg-[#0d1117] flex flex-col items-center justify-center gap-8 p-6">
<!-- Single like button -->
<div class="relative flex flex-col items-center">
<button
@click="toggle"
:aria-label="liked ? 'Unlike' : 'Like'"
class="relative flex items-center gap-2.5 px-6 py-3 rounded-full border transition-all duration-200 select-none"
:style="{
background: liked ? 'rgba(255,107,107,0.12)' : 'rgba(255,255,255,0.04)',
borderColor: liked ? 'rgba(255,107,107,0.4)' : 'rgba(255,255,255,0.1)',
transform: burst ? 'scale(0.93)' : 'scale(1)',
}"
>
<svg
width="22" height="22" viewBox="0 0 24 24"
:fill="liked ? '#ff6b6b' : 'none'"
:stroke="liked ? '#ff6b6b' : '#8b949e'"
stroke-width="2"
:style="{
transition: 'all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1)',
transform: liked ? 'scale(1.2)' : 'scale(1)',
}"
>
<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>
<span
class="font-semibold text-sm tabular-nums transition-colors"
:style="{ color: liked ? '#ff6b6b' : '#8b949e' }"
>
{{ count.toLocaleString() }}
</span>
</button>
<!-- Burst particles -->
<span
v-if="burst"
v-for="(p, i) in PARTICLES"
:key="i"
class="absolute w-2 h-2 rounded-full pointer-events-none"
:style="{
background: p.color,
animation: 'particle-burst 0.6s ease-out both',
'--angle': p.angle + 'deg',
}"
></span>
</div>
<!-- Variants showcase -->
<div class="grid grid-cols-3 gap-4">
<button
v-for="(v, i) in variants"
:key="v.label"
@click="toggleVariant(i)"
class="flex flex-col items-center gap-1.5 py-3 px-4 rounded-xl border transition-all duration-200"
:style="{
background: variantStates[i].active ? v.color + '18' : 'rgba(255,255,255,0.03)',
borderColor: variantStates[i].active ? v.color + '40' : 'rgba(255,255,255,0.08)',
transform: variantStates[i].active ? 'scale(1.05)' : 'scale(1)',
}"
>
<span
class="text-2xl"
:style="{
filter: variantStates[i].active ? 'none' : 'grayscale(1) opacity(0.4)',
transition: 'filter 0.2s',
}"
>
{{ v.icon }}
</span>
<span
class="text-xs font-semibold tabular-nums"
:style="{ color: variantStates[i].active ? v.color : '#8b949e' }"
>
{{ variantStates[i].count.toLocaleString() }}
</span>
<span class="text-[10px] text-[#484f58]">{{ v.label }}</span>
</button>
</div>
</div>
</template>
<style scoped>
@keyframes particle-burst {
0% { transform: translate(0,0) scale(1); opacity: 1; }
100% { transform: translate(calc(cos(var(--angle)) * 32px), calc(sin(var(--angle)) * 32px)) scale(0); opacity: 0; }
}
</style>Animated Like Button
Capture user delight with a satisfying like button animation. This component uses CSS keyframes for a vibrant “pop” effect when activated.
Features
- Heart icon with SVG paths
- Active/Inactive state toggling
- Splash/Particle animation on click
- Highly customizable colors
- Accessible ARIA labels