Like Button
Original · freeA like button that toggles with a heart burst and a live count.
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/btn-like.json"use client";
import { useState, useId } from "react";
import { motion, AnimatePresence, useReducedMotion } from "motion/react";
type Size = "sm" | "md" | "lg";
const SIZES: Record<
Size,
{ pad: string; icon: string; text: string; gap: string; count: string }
> = {
sm: { pad: "px-3 py-1.5", icon: "h-4 w-4", text: "text-xs", gap: "gap-1.5", count: "text-xs" },
md: { pad: "px-4 py-2", icon: "h-5 w-5", text: "text-sm", gap: "gap-2", count: "text-sm" },
lg: { pad: "px-5 py-2.5", icon: "h-6 w-6", text: "text-base", gap: "gap-2.5", count: "text-base" },
};
function Heart({ filled, className }: { filled: boolean; className: string }) {
return (
<svg
viewBox="0 0 24 24"
className={className}
fill={filled ? "currentColor" : "none"}
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<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 1 0-7.78 7.78L12 21.23l8.84-8.84a5.5 5.5 0 0 0 0-7.78z" />
</svg>
);
}
const BURST = [
{ x: 0, y: -22, s: 1, d: 0 },
{ x: 18, y: -12, s: 0.7, d: 0.02 },
{ x: 22, y: 6, s: 0.55, d: 0.04 },
{ x: 12, y: 20, s: 0.8, d: 0.01 },
{ x: -12, y: 20, s: 0.6, d: 0.03 },
{ x: -22, y: 6, s: 0.75, d: 0.05 },
{ x: -18, y: -12, s: 0.6, d: 0.02 },
];
function formatCount(n: number): string {
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1).replace(/\.0$/, "") + "M";
if (n >= 1_000) return (n / 1_000).toFixed(1).replace(/\.0$/, "") + "K";
return String(n);
}
function LikeButton({
size = "md",
base = 0,
initiallyLiked = false,
label = "Like",
}: {
size?: Size;
base?: number;
initiallyLiked?: boolean;
label?: string;
}) {
const reduce = useReducedMotion();
const [liked, setLiked] = useState(initiallyLiked);
const [burstKey, setBurstKey] = useState(0);
const s = SIZES[size];
const count = base + (liked ? 1 : 0);
const toggle = () => {
setLiked((v) => {
if (!v) setBurstKey((k) => k + 1);
return !v;
});
};
return (
<button
type="button"
onClick={toggle}
aria-pressed={liked}
aria-label={liked ? "Unlike" : label}
className={[
"group relative inline-flex select-none items-center rounded-full font-semibold",
"border transition-all duration-200 active:scale-[0.96]",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
"focus-visible:ring-rose-500 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950",
s.pad,
s.text,
s.gap,
liked
? "border-rose-200 bg-rose-50 text-rose-600 dark:border-rose-500/30 dark:bg-rose-500/10 dark:text-rose-400"
: "border-slate-200 bg-white text-slate-600 hover:border-rose-200 hover:text-rose-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300 dark:hover:border-rose-500/40 dark:hover:text-rose-400",
].join(" ")}
>
<span className="relative inline-flex items-center justify-center">
<motion.span
key={liked ? "on" : "off"}
initial={reduce ? false : { scale: liked ? 0.4 : 1 }}
animate={{ scale: 1 }}
transition={
reduce
? { duration: 0 }
: { type: "spring", stiffness: 600, damping: 15, mass: 0.5 }
}
className="inline-flex"
>
<Heart filled={liked} className={s.icon} />
</motion.span>
<AnimatePresence>
{liked && !reduce && (
<span
key={burstKey}
className="pointer-events-none absolute inset-0 flex items-center justify-center"
>
<motion.span
initial={{ scale: 0, opacity: 0.7 }}
animate={{ scale: 2.2, opacity: 0 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.5, ease: "easeOut" }}
className="absolute h-6 w-6 rounded-full bg-rose-400/40"
/>
{BURST.map((p, i) => (
<motion.span
key={i}
initial={{ x: 0, y: 0, scale: 0, opacity: 1 }}
animate={{ x: p.x, y: p.y, scale: p.s, opacity: 0 }}
transition={{ duration: 0.55, delay: p.d, ease: "easeOut" }}
className="absolute h-1.5 w-1.5 rounded-full bg-rose-500"
/>
))}
</span>
)}
</AnimatePresence>
</span>
<span className="relative overflow-hidden">
<span className="tabular-nums" aria-hidden="true">
<AnimatePresence mode="popLayout" initial={false}>
<motion.span
key={count}
initial={reduce ? false : { y: liked ? 10 : -10, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={reduce ? { opacity: 0 } : { y: liked ? -10 : 10, opacity: 0 }}
transition={{ duration: 0.2 }}
className={["inline-block", s.count].join(" ")}
>
{formatCount(count)}
</motion.span>
</AnimatePresence>
</span>
</span>
<span className="sr-only" aria-live="polite">
{formatCount(count)} likes
</span>
</button>
);
}
function IconLikeButton({
size = "md",
initiallyLiked = false,
}: {
size?: Size;
initiallyLiked?: boolean;
}) {
const reduce = useReducedMotion();
const [liked, setLiked] = useState(initiallyLiked);
const [burstKey, setBurstKey] = useState(0);
const dim = size === "sm" ? "h-9 w-9" : size === "lg" ? "h-14 w-14" : "h-11 w-11";
const ic = SIZES[size].icon;
return (
<button
type="button"
onClick={() =>
setLiked((v) => {
if (!v) setBurstKey((k) => k + 1);
return !v;
})
}
aria-pressed={liked}
aria-label={liked ? "Unlike" : "Like"}
className={[
"relative inline-flex items-center justify-center rounded-full border transition-all duration-200 active:scale-90",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-500 focus-visible:ring-offset-2",
"focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950",
dim,
liked
? "border-rose-200 bg-rose-500 text-white shadow-lg shadow-rose-500/25 dark:border-rose-500/40"
: "border-slate-200 bg-white text-slate-500 hover:text-rose-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-400 dark:hover:text-rose-400",
].join(" ")}
>
<motion.span
key={liked ? "on" : "off"}
initial={reduce ? false : { scale: liked ? 0.4 : 1 }}
animate={{ scale: 1 }}
transition={
reduce ? { duration: 0 } : { type: "spring", stiffness: 600, damping: 14, mass: 0.5 }
}
className="inline-flex"
>
<Heart filled={liked} className={ic} />
</motion.span>
<AnimatePresence>
{liked && !reduce && (
<span
key={burstKey}
className="pointer-events-none absolute inset-0 flex items-center justify-center"
>
{BURST.map((p, i) => (
<motion.span
key={i}
initial={{ x: 0, y: 0, scale: 0, opacity: 1 }}
animate={{ x: p.x, y: p.y, scale: p.s, opacity: 0 }}
transition={{ duration: 0.6, delay: p.d, ease: "easeOut" }}
className="absolute h-1.5 w-1.5 rounded-full bg-rose-400"
/>
))}
</span>
)}
</AnimatePresence>
</button>
);
}
function PillLikeButton() {
const reduce = useReducedMotion();
const [liked, setLiked] = useState(true);
const [burstKey, setBurstKey] = useState(0);
const count = 2847 + (liked ? 0 : -1);
return (
<button
type="button"
onClick={() =>
setLiked((v) => {
if (!v) setBurstKey((k) => k + 1);
return !v;
})
}
aria-pressed={liked}
aria-label={liked ? "Remove like from “Midnight Skyline” print" : "Like “Midnight Skyline” print"}
className={[
"group relative inline-flex items-center gap-2.5 rounded-full px-5 py-2.5 text-sm font-semibold",
"transition-all duration-200 active:scale-[0.97]",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
"focus-visible:ring-rose-500 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950",
liked
? "bg-gradient-to-r from-rose-500 to-pink-500 text-white shadow-lg shadow-rose-500/30"
: "bg-slate-100 text-slate-700 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-200 dark:hover:bg-slate-700",
].join(" ")}
>
<span className="relative inline-flex">
<motion.span
key={liked ? "on" : "off"}
initial={reduce ? false : { scale: liked ? 0.4 : 1 }}
animate={{ scale: 1 }}
transition={reduce ? { duration: 0 } : { type: "spring", stiffness: 600, damping: 15 }}
className="inline-flex"
>
<Heart filled={liked} className="h-5 w-5" />
</motion.span>
<AnimatePresence>
{liked && !reduce && (
<span key={burstKey} className="pointer-events-none absolute inset-0 flex items-center justify-center">
{BURST.map((p, i) => (
<motion.span
key={i}
initial={{ x: 0, y: 0, scale: 0, opacity: 1 }}
animate={{ x: p.x, y: p.y, scale: p.s, opacity: 0 }}
transition={{ duration: 0.55, delay: p.d, ease: "easeOut" }}
className="absolute h-1.5 w-1.5 rounded-full bg-white"
/>
))}
</span>
)}
</AnimatePresence>
</span>
<span>{liked ? "Liked" : "Like"}</span>
<span className="h-4 w-px bg-current opacity-20" aria-hidden="true" />
<span className="tabular-nums" aria-hidden="true">
{formatCount(count)}
</span>
<span className="sr-only" aria-live="polite">
{formatCount(count)} people liked this print
</span>
</button>
);
}
function PhotoCardLike() {
const reduce = useReducedMotion();
const [liked, setLiked] = useState(false);
const [burstKey, setBurstKey] = useState(0);
const count = 1204 + (liked ? 1 : 0);
const capId = useId();
const like = () =>
setLiked((v) => {
if (!v) setBurstKey((k) => k + 1);
return !v;
});
return (
<figure
className="w-64 overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900"
aria-labelledby={capId}
>
<div className="relative">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src="/img/gallery/g14.webp"
alt="Aerial photograph of a winding coastal road at golden hour"
loading="lazy"
draggable={false}
className="h-40 w-full object-cover"
/>
<div className="absolute right-3 top-3">
<IconLikeButton size="sm" />
</div>
</div>
<figcaption id={capId} className="flex items-center justify-between gap-2 p-3">
<div className="min-w-0">
<p className="truncate text-sm font-semibold text-slate-900 dark:text-slate-50">
Coastal Route 1
</p>
<p className="truncate text-xs text-slate-500 dark:text-slate-400">by Elena Marsh</p>
</div>
<button
type="button"
onClick={like}
aria-pressed={liked}
aria-label={liked ? "Unlike Coastal Route 1" : "Like Coastal Route 1"}
className={[
"relative inline-flex shrink-0 items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-semibold transition-colors",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-500 focus-visible:ring-offset-2",
"focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900",
liked
? "text-rose-500 dark:text-rose-400"
: "text-slate-500 hover:text-rose-500 dark:text-slate-400 dark:hover:text-rose-400",
].join(" ")}
>
<span className="relative inline-flex">
<Heart filled={liked} className="h-4 w-4" />
<AnimatePresence>
{liked && !reduce && (
<span key={burstKey} className="pointer-events-none absolute inset-0 flex items-center justify-center">
{BURST.map((p, i) => (
<motion.span
key={i}
initial={{ x: 0, y: 0, scale: 0, opacity: 1 }}
animate={{ x: p.x * 0.7, y: p.y * 0.7, scale: p.s, opacity: 0 }}
transition={{ duration: 0.5, delay: p.d, ease: "easeOut" }}
className="absolute h-1 w-1 rounded-full bg-rose-500"
/>
))}
</span>
)}
</AnimatePresence>
</span>
<span className="tabular-nums">{formatCount(count)}</span>
</button>
</figcaption>
</figure>
);
}
export default function BtnLike() {
return (
<section className="relative w-full bg-white px-6 py-20 dark:bg-slate-950 sm:px-10">
<style>{`
@media (prefers-reduced-motion: reduce) {
.btnlike-hint { animation: none !important; }
}
@keyframes btnlike-pulse {
0%, 100% { opacity: 0.35; }
50% { opacity: 0.9; }
}
.btnlike-hint { animation: btnlike-pulse 2.4s ease-in-out infinite; }
`}</style>
<div className="mx-auto max-w-3xl">
<header className="mb-14 text-center">
<p className="btnlike-hint mb-3 text-xs font-semibold uppercase tracking-[0.2em] text-rose-500 dark:text-rose-400">
FreeCode / Buttons
</p>
<h2 className="text-3xl font-bold tracking-tight text-slate-900 dark:text-slate-50 sm:text-4xl">
Like Button
</h2>
<p className="mx-auto mt-3 max-w-md text-sm text-slate-600 dark:text-slate-400">
Tap to toggle. Every like fires a heart burst and bumps a live, formatted count.
</p>
</header>
<div className="space-y-12">
<div>
<h3 className="mb-4 text-xs font-semibold uppercase tracking-wider text-slate-400 dark:text-slate-500">
With count · sizes
</h3>
<div className="flex flex-wrap items-center gap-4 rounded-2xl border border-slate-200 bg-slate-50 p-6 dark:border-slate-800 dark:bg-slate-900/50">
<LikeButton size="sm" base={128} />
<LikeButton size="md" base={2400} initiallyLiked />
<LikeButton size="lg" base={18900} label="Like this post" />
</div>
</div>
<div>
<h3 className="mb-4 text-xs font-semibold uppercase tracking-wider text-slate-400 dark:text-slate-500">
Icon only
</h3>
<div className="flex flex-wrap items-center gap-4 rounded-2xl border border-slate-200 bg-slate-50 p-6 dark:border-slate-800 dark:bg-slate-900/50">
<IconLikeButton size="sm" />
<IconLikeButton size="md" initiallyLiked />
<IconLikeButton size="lg" />
</div>
</div>
<div>
<h3 className="mb-4 text-xs font-semibold uppercase tracking-wider text-slate-400 dark:text-slate-500">
Gradient pill
</h3>
<div className="flex flex-wrap items-center gap-4 rounded-2xl border border-slate-200 bg-slate-50 p-6 dark:border-slate-800 dark:bg-slate-900/50">
<PillLikeButton />
</div>
</div>
<div>
<h3 className="mb-4 text-xs font-semibold uppercase tracking-wider text-slate-400 dark:text-slate-500">
In context
</h3>
<div className="flex flex-wrap items-center gap-4 rounded-2xl border border-slate-200 bg-slate-50 p-6 dark:border-slate-800 dark:bg-slate-900/50">
<PhotoCardLike />
</div>
</div>
</div>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Shimmer Button
MITA dark pill button with a continuous conic-gradient light shimmer sweeping around its border.

Rainbow Gradient Button
MITA button wrapped in an animated multi-colour gradient border with a matching blurred underglow.

Interactive Hover Button
MITA pill button whose dot expands to fill the surface and reveals an arrow label on hover.

Pulsating Button
MITA solid button that emits a soft, continuously pulsing glow ring to draw the eye to the primary action.

Shiny Button
MITA frosted-glass button with a spring-animated light sweeping across its text and border.

Solid Set Button
OriginalA set of solid buttons: primary, secondary, success and danger, in three sizes.

Outline Set Button
OriginalOutline button variants across colours and sizes with a hover fill.

Ghost Set Button
OriginalGhost and text buttons with subtle hover backgrounds.

Soft Set Button
OriginalSoft, tinted buttons across semantic colours.

Gradient Set Button
OriginalGradient-filled buttons with a hover shift.
Icon Leading Button
OriginalButtons with leading and trailing icons.
Icon Only Button
OriginalSquare and circular icon-only buttons with tooltips and aria-labels.

