Prompt
Build a favorite (heart) button with a playful burst animation: when it turns ON, the heart pops, a lime ring pulses out, and 10 tiny lime and purple pieces fly outward and fade. Only the button and its animation; wire onToggle to your own storage.
Stack: Next.js (App Router), React, TypeScript and Tailwind CSS v4. No animation library: React state plus CSS keyframes.
COMPONENT: FavoriteButton({ title, defaultOn = false, onToggle, size = 22, className, activeClass = "text-lime" }) — a client component.
- State: on (boolean, starts at defaultOn) and burst (a number, starts at 0).
- Click: e.preventDefault() and e.stopPropagation() (so it works inside a clickable card). If it's turning ON, do burst + 1. Then flip on and call onToggle?.(nextValue).
- <button type="button">: aria-pressed={on}; aria-label "Add {title} to favorites" / "Remove {title} from favorites"; title "Add to favorites" / "Remove from favorites".
- Button classes: relative, z-index 10, flex, shrink-0, items/justify center, plus className, plus activeClass while on.
- Inside a clickable card, give it -m-2 p-2 so the tap area is bigger without moving the layout.
HEART
- SVG 24×24 viewBox at size×size, stroke currentColor, stroke-width 1.5, stroke-linejoin round. fill currentColor when on, none when off.
- Path: M12 20.5s-7.5-4.6-9.2-9.4C1.7 7.9 3.8 4.5 7.2 4.5c2 0 3.5 1.1 4.8 2.8 1.3-1.7 2.8-2.8 4.8-2.8 3.4 0 5.5 3.4 4.4 6.6-1.7 4.8-9.2 9.4-9.2 9.4Z
- Wrap the SVG in <span key={burst} className="flex"> and add the class "fav-pop" when burst > 0 and on. The changing key remounts the span, so the pop replays on every new favorite.
BURST (render only when burst > 0 and on)
- <span key={"b" + burst} className="fav-burst" aria-hidden="true">: pointer-events none, position absolute, left 50%, top 50%.
- Inside: one <span className="fav-ring" /> and 10 pieces.
- Pieces: for i = 0..9, angle = (i / 10) × 2π − π/2 (the first one points straight up). Distance = 22px for odd i, 30px for even i.
- --tx = cos(angle) × distance px, --ty = sin(angle) × distance px (rounded to 1 decimal), set as inline CSS variables.
- Color: #c084fc (purple) for odd i, #a3f207 (lime) for even i.
- Every 3rd piece (i % 3 === 0) is a 9×9 SVG of the same heart path, filled with the color. The others are 6×6 round dots (size-1.5, rounded-full) with that background.
- animation-delay: (i % 3) × 25ms.
CSS (global)
@keyframes fav-pop { 0% scale(1); 25% scale(0.75); 55% scale(1.35); 80% scale(0.95); 100% scale(1) }
.fav-pop { animation: fav-pop 0.5s cubic-bezier(0.3, 1.5, 0.5, 1); }
@keyframes fav-ring { from { transform: translate(-50%, -50%) scale(0.3); opacity: 0.9 } to { transform: translate(-50%, -50%) scale(1.9); opacity: 0 } }
.fav-ring { position: absolute; left: 0; top: 0; width: 34px; height: 34px; border-radius: 9999px; border: 2px solid #a3f207; animation: fav-ring 0.5s ease-out forwards; }
@keyframes fav-piece {
0% { transform: translate(-50%, -50%) scale(0.2); opacity: 1 }
60% { opacity: 1 }
100% { transform: translate(calc(-50% + var(--tx)), calc(-50% + var(--ty))) scale(1); opacity: 0 }
}
.fav-piece { position: absolute; left: 0; top: 0; opacity: 0; animation: fav-piece 0.65s cubic-bezier(0.2, 0.7, 0.3, 1) forwards; }
REDUCED MOTION
- @media (prefers-reduced-motion: reduce): no animation on .fav-pop, .fav-ring and .fav-piece; hide the ring and pieces; keep the heart visible (display flex).
COLORS
- Off: the heart is an outline in the surrounding text color. On: filled with activeClass (lime #a3f207 on dark cards). On a purple header, use a white 56px round button with a purple fill when on.
- The color changes only when the state changes, never on hover.More in Animations
GSAP split-text hover: each letter rolls up and is replaced by its twin, letter by letter, and always finishes.
Letter Roll Hover Text
Rounded Figma-style arrow plus a lagging ring of rotating text, blended with difference mode.
Custom Cursor with Text Ring
A glowing yellow band of light that sweeps left to right through the letters — not a static glow.
Shine Sweep Text
A rounded, inset section that grows to the screen edges (and upward) as you scroll, then eases back partway as it leaves.
Scroll-Grow Section
An endless, seamless row of tech pills that fades out at both edges — pure CSS, no JavaScript.