Animated like buttons that burst with joy — add delightful feedback to any interaction
Icon
Burst Style
Copy URL or Code
Paste to your AI coding assistant and customize:
Done. Your AI handles the rest.
Fully customizable. 5 icon types, 5 burst styles, optional like counter. Pure React + Canvas, no external dependencies.
/*
================================================================================
AI COMPONENT: PoppyHearts
================================================================================
SETUP:
1. Create file: components/PoppyHearts.tsx
2. Copy this entire code block into that file
3. Import: import PoppyHearts from "@/components/PoppyHearts"
4. No external dependencies required - pure React + Canvas
================================================================================
QUICK CUSTOMIZATION (via props)
================================================================================
| Prop | Type | Default |
|----------------|---------------------------------------------------------|------------|
| icon | "heart" | "thumbsUp" | "star" | "fire" | "sparkle" | "heart" |
| burstStyle | "confetti" | "rings" | "swirl" | "lines" | "bubbles" | "confetti" |
| size | number (pixels) | 64 |
| showCount | boolean | false |
| initialCount | number | 0 |
| theme.primary | string (hex) | "#FF6B9D" |
| theme.secondary| string (hex) | "#C084FC" |
| onToggle | (active: boolean, count: number) => void | undefined |
================================================================================
ICON TYPES
================================================================================
| Icon | Description |
|-----------|--------------------------------------------|
| heart | Classic heart shape (default) |
| thumbsUp | Thumbs up / like icon |
| star | Star rating icon |
| fire | Fire / trending icon |
| sparkle | Star sparkle / favorite icon |
================================================================================
BURST STYLES
================================================================================
| Style | Description |
|-----------|--------------------------------------------|
| confetti | Colorful circular particles (default) |
| rings | Expanding ring particles |
| swirl | Swirling spiral particles |
| lines | Line-shaped particles |
| bubbles | Larger bubble-like particles |
================================================================================
DEFAULT THEME COLORS
================================================================================
- primary: "#FF6B9D" (pink)
- secondary: "#C084FC" (purple)
- tertiary: "#60A5FA" (blue)
- quaternary: "#34D399" (green)
- inactive: "#9CA3AF" (gray)
================================================================================
COMMON MODIFICATIONS
================================================================================
1. BASIC HEART BUTTON:
<PoppyHearts />
2. WITH LIKE COUNTER:
<PoppyHearts showCount initialCount={42} />
3. DIFFERENT ICON:
<PoppyHearts icon="star" />
4. DIFFERENT BURST STYLE:
<PoppyHearts burstStyle="rings" />
5. CUSTOM SIZE:
<PoppyHearts size={100} />
6. CUSTOM COLORS:
<PoppyHearts theme={{ primary: "#FF0000", secondary: "#00FF00" }} />
7. WITH CALLBACK:
<PoppyHearts onToggle={(active, count) => console.log(active, count)} />
================================================================================
SOURCE CODE
================================================================================
*/
"use client";
import React, { useRef, useEffect, useState, useCallback } from "react";
export type IconType = "heart" | "thumbsUp" | "star" | "fire" | "sparkle";
export type BurstStyle = "confetti" | "rings" | "swirl" | "lines" | "bubbles";
export interface PoppyHeartsTheme {
primary?: string;
secondary?: string;
tertiary?: string;
quaternary?: string;
inactive?: string;
}
export interface PoppyHeartsProps {
icon?: IconType;
burstStyle?: BurstStyle;
size?: number;
theme?: PoppyHeartsTheme;
showCount?: boolean;
initialCount?: number;
className?: string;
onToggle?: (active: boolean, count: number) => void;
}
const defaultTheme: Required<PoppyHeartsTheme> = {
primary: "#FF6B9D",
secondary: "#C084FC",
tertiary: "#60A5FA",
quaternary: "#34D399",
inactive: "#9CA3AF",
};
const icons: Record<IconType, (size: number) => React.ReactNode> = {
heart: (size) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor">
<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>
),
thumbsUp: (size) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor">
<path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/>
</svg>
),
star: (size) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor">
<path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/>
</svg>
),
fire: (size) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor">
<path d="M13.5.67s.74 2.65.74 4.8c0 2.06-1.35 3.73-3.41 3.73-2.07 0-3.63-1.67-3.63-3.73l.03-.36C5.21 7.51 4 10.62 4 14c0 4.42 3.58 8 8 8s8-3.58 8-8C20 8.61 17.41 3.8 13.5.67zM11.71 19c-1.78 0-3.22-1.4-3.22-3.14 0-1.62 1.05-2.76 2.81-3.12 1.77-.36 3.6-1.21 4.62-2.58.39 1.29.59 2.65.59 4.04 0 2.65-2.15 4.8-4.8 4.8z"/>
</svg>
),
sparkle: (size) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2L9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2z"/>
</svg>
),
};
interface Particle {
id: number;
x: number;
y: number;
vx: number;
vy: number;
size: number;
color: string;
opacity: number;
rotation: number;
rotationSpeed: number;
life: number;
maxLife: number;
shape: "circle" | "line" | "ring";
}
export default function PoppyHearts({
icon = "heart",
burstStyle = "confetti",
size = 64,
theme: customTheme,
showCount = false,
initialCount = 0,
className = "",
onToggle,
}: PoppyHeartsProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const animationRef = useRef<number | undefined>(undefined);
const particlesRef = useRef<Particle[]>([]);
const [active, setActive] = useState(false);
const [count, setCount] = useState(initialCount);
const [scale, setScale] = useState(1);
const [ringProgress, setRingProgress] = useState(0);
const theme = { ...defaultTheme, ...customTheme };
const colors = [theme.primary, theme.secondary, theme.tertiary, theme.quaternary];
const iconSize = size * 0.5;
const canvasSize = size * 3;
const createParticles = useCallback(() => {
const centerX = canvasSize / 2;
const centerY = canvasSize / 2;
const particles: Particle[] = [];
const particleCount = burstStyle === "bubbles" ? 8 : burstStyle === "rings" ? 12 : 20;
for (let i = 0; i < particleCount; i++) {
const angle = (i / particleCount) * Math.PI * 2;
const speed = 3 + Math.random() * 4;
let vx = Math.cos(angle) * speed;
let vy = Math.sin(angle) * speed;
if (burstStyle === "swirl") {
const swirlAngle = angle + (Math.random() - 0.5) * 0.5;
vx = Math.cos(swirlAngle) * speed * (Math.random() > 0.5 ? 1 : 0.7);
vy = Math.sin(swirlAngle) * speed * (Math.random() > 0.5 ? 1 : 0.7);
}
particles.push({
id: i,
x: centerX,
y: centerY,
vx,
vy,
size: burstStyle === "lines" ? 12 : burstStyle === "bubbles" ? 8 + Math.random() * 6 : 4 + Math.random() * 4,
color: colors[i % colors.length],
opacity: 1,
rotation: angle,
rotationSpeed: (Math.random() - 0.5) * 0.2,
life: 0,
maxLife: 40 + Math.random() * 20,
shape: burstStyle === "lines" ? "line" : burstStyle === "rings" ? "ring" : "circle",
});
}
particlesRef.current = particles;
}, [burstStyle, canvasSize, colors]);
const animate = useCallback(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
ctx.clearRect(0, 0, canvasSize, canvasSize);
// Draw ring
if (ringProgress > 0 && ringProgress < 1) {
const centerX = canvasSize / 2;
const centerY = canvasSize / 2;
const ringRadius = size * 0.5 + ringProgress * size * 0.8;
const strokeWidth = Math.max(0, 15 * (1 - ringProgress));
ctx.beginPath();
ctx.arc(centerX, centerY, ringRadius, 0, Math.PI * 2);
ctx.strokeStyle = theme.primary;
ctx.globalAlpha = (1 - ringProgress) * 0.6;
ctx.lineWidth = strokeWidth;
ctx.stroke();
ctx.globalAlpha = 1;
}
// Update and draw particles
const particles = particlesRef.current;
let hasActiveParticles = false;
particles.forEach((p) => {
if (p.life < p.maxLife) {
hasActiveParticles = true;
p.life++;
p.x += p.vx;
p.y += p.vy;
p.vy += 0.05; // gravity
p.vx *= 0.98; // friction
p.vy *= 0.98;
p.rotation += p.rotationSpeed;
p.opacity = 1 - (p.life / p.maxLife);
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(p.rotation);
ctx.globalAlpha = p.opacity * 0.8;
ctx.fillStyle = p.color;
ctx.strokeStyle = p.color;
if (p.shape === "circle") {
ctx.beginPath();
ctx.arc(0, 0, p.size, 0, Math.PI * 2);
ctx.fill();
} else if (p.shape === "line") {
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, -p.size / 2);
ctx.lineTo(0, p.size / 2);
ctx.stroke();
} else if (p.shape === "ring") {
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(0, 0, p.size, 0, Math.PI * 2);
ctx.stroke();
}
ctx.restore();
}
});
if (hasActiveParticles || ringProgress > 0) {
animationRef.current = requestAnimationFrame(animate);
}
}, [canvasSize, ringProgress, size, theme.primary]);
useEffect(() => {
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}, []);
const handleClick = useCallback(() => {
const newActive = !active;
setActive(newActive);
if (newActive) {
// Trigger animations
createParticles();
setRingProgress(0.01);
// Animate scale
setScale(0);
setTimeout(() => setScale(1.3), 50);
setTimeout(() => setScale(1), 200);
// Animate ring
let ringStart = 0;
const ringAnimate = () => {
ringStart += 0.04;
setRingProgress(ringStart);
if (ringStart < 1) {
requestAnimationFrame(ringAnimate);
} else {
setRingProgress(0);
}
};
ringAnimate();
// Start particle animation
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
animationRef.current = requestAnimationFrame(animate);
if (showCount) {
setCount((c) => c + 1);
}
} else {
// Reset scale on deactivate
setScale(0.8);
setTimeout(() => setScale(1), 100);
if (showCount) {
setCount((c) => Math.max(0, c - 1));
}
}
onToggle?.(newActive, showCount ? (newActive ? count + 1 : Math.max(0, count - 1)) : count);
}, [active, animate, count, createParticles, onToggle, showCount]);
const dpr = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
return (
<div
ref={containerRef}
className={`relative inline-flex items-center justify-center cursor-pointer select-none ${className}`}
onClick={handleClick}
style={{ width: size, height: size }}
>
{/* Canvas for particles */}
<canvas
ref={canvasRef}
width={canvasSize * dpr}
height={canvasSize * dpr}
className="absolute pointer-events-none"
style={{
width: canvasSize,
height: canvasSize,
left: "50%",
top: "50%",
transform: "translate(-50%, -50%)",
}}
/>
{/* Icon */}
<div
className="relative z-10 transition-transform"
style={{
transform: `scale(${scale})`,
transitionDuration: scale === 1.3 ? "150ms" : "100ms",
transitionTimingFunction: scale === 1.3 ? "cubic-bezier(0.175, 0.885, 0.32, 1.275)" : "ease-out",
color: active ? theme.primary : theme.inactive,
}}
>
{icons[icon](iconSize)}
</div>
{/* Count */}
{showCount && (
<span
className="absolute -right-1 -top-1 text-xs font-semibold px-1.5 py-0.5 rounded-full min-w-[20px] text-center"
style={{
backgroundColor: active ? theme.primary : theme.inactive,
color: "white",
}}
>
{count}
</span>
)}
</div>
);
}
export { PoppyHearts };import PoppyHearts from "@/components/PoppyHearts";
import { useState } from "react";
// Icons: "heart", "thumbsUp", "star", "fire", "sparkle"
// Burst styles: "confetti", "rings", "swirl", "lines", "bubbles"
// Basic heart like button
export default function BasicHeart() {
return (
<div className="flex items-center justify-center min-h-screen">
<PoppyHearts />
</div>
);
}
// Heart with like counter
export function HeartWithCounter() {
return (
<div className="flex items-center justify-center min-h-screen">
<PoppyHearts showCount initialCount={42} />
</div>
);
}
// Star rating with rings burst
export function StarRating() {
return (
<div className="flex items-center justify-center min-h-screen gap-4">
<PoppyHearts icon="star" burstStyle="rings" />
<PoppyHearts icon="star" burstStyle="rings" />
<PoppyHearts icon="star" burstStyle="rings" />
<PoppyHearts icon="star" burstStyle="rings" />
<PoppyHearts icon="star" burstStyle="rings" />
</div>
);
}
// Custom themed fire button
export function FireButton() {
return (
<div className="flex items-center justify-center min-h-screen">
<PoppyHearts
icon="fire"
burstStyle="swirl"
size={100}
theme={{
primary: "#FF6B35",
secondary: "#F7C815",
tertiary: "#FF9F1C",
quaternary: "#E63946",
}}
/>
</div>
);
}
// With callback for tracking
export function TrackedLike() {
const [likes, setLikes] = useState(0);
return (
<div className="flex flex-col items-center justify-center min-h-screen gap-4">
<PoppyHearts
icon="thumbsUp"
showCount
initialCount={likes}
onToggle={(active, count) => {
console.log("Liked:", active, "Total:", count);
setLikes(count);
}}
/>
<p>Total likes: {likes}</p>
</div>
);
}