Icon Fill Hover Effect
Original · freeIcon buttons whose icon and background fill on hover.
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/hover-icon-fill.json"use client";
import { useState, type ReactElement } from "react";
type IconName =
| "heart"
| "star"
| "bookmark"
| "bell"
| "share"
| "download"
| "play"
| "link";
type IconButton = {
id: string;
name: IconName;
label: string;
activeLabel: string;
fill: string;
ring: string;
text: string;
darkText: string;
};
const ICONS: Record<IconName, (props: { className?: string }) => ReactElement> = {
heart: ({ className }) => (
<svg
viewBox="0 0 24 24"
fill="currentColor"
stroke="currentColor"
strokeWidth="1.75"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
aria-hidden="true"
>
<path d="M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.6l-1-1a5.5 5.5 0 0 0-7.8 7.8l1 1L12 21.2l7.8-7.8 1-1a5.5 5.5 0 0 0 0-7.8Z" />
</svg>
),
star: ({ className }) => (
<svg
viewBox="0 0 24 24"
fill="currentColor"
stroke="currentColor"
strokeWidth="1.75"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
aria-hidden="true"
>
<path d="M12 2.5l2.9 5.9 6.5.95-4.7 4.58 1.1 6.47L12 17.4 6.2 20.9l1.1-6.47-4.7-4.58 6.5-.95L12 2.5Z" />
</svg>
),
bookmark: ({ className }) => (
<svg
viewBox="0 0 24 24"
fill="currentColor"
stroke="currentColor"
strokeWidth="1.75"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
aria-hidden="true"
>
<path d="M6 3.5h12a1 1 0 0 1 1 1v16l-7-4.2-7 4.2v-16a1 1 0 0 1 1-1Z" />
</svg>
),
bell: ({ className }) => (
<svg
viewBox="0 0 24 24"
fill="currentColor"
stroke="currentColor"
strokeWidth="1.75"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
aria-hidden="true"
>
<path d="M6 9a6 6 0 0 1 12 0c0 5 2 6.5 2 6.5H4S6 14 6 9Z" />
<path d="M10.5 19.5a2 2 0 0 0 3 0" fill="none" />
</svg>
),
share: ({ className }) => (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.85"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
aria-hidden="true"
>
<circle cx="6" cy="12" r="2.6" fill="currentColor" />
<circle cx="18" cy="6" r="2.6" fill="currentColor" />
<circle cx="18" cy="18" r="2.6" fill="currentColor" />
<path d="M8.3 10.8 15.7 7.2M8.3 13.2l7.4 3.6" />
</svg>
),
download: ({ className }) => (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.85"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
aria-hidden="true"
>
<path d="M12 3.5v11" />
<path d="m7.5 10.5 4.5 4.5 4.5-4.5" />
<path d="M4.5 19.5h15" />
</svg>
),
play: ({ className }) => (
<svg
viewBox="0 0 24 24"
fill="currentColor"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
aria-hidden="true"
>
<path d="M8 5.2 19 12 8 18.8V5.2Z" />
</svg>
),
link: ({ className }) => (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.85"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
aria-hidden="true"
>
<path d="M9.5 14.5a3.5 3.5 0 0 0 5 0l3-3a3.54 3.54 0 0 0-5-5l-1 1" />
<path d="M14.5 9.5a3.5 3.5 0 0 0-5 0l-3 3a3.54 3.54 0 0 0 5 5l1-1" />
</svg>
),
};
const BUTTONS: IconButton[] = [
{
id: "like",
name: "heart",
label: "Like this post",
activeLabel: "Liked",
fill: "bg-rose-500",
ring: "focus-visible:ring-rose-500/60 group-hover:border-rose-500/70",
text: "text-rose-600",
darkText: "dark:text-rose-400",
},
{
id: "favorite",
name: "star",
label: "Add to favorites",
activeLabel: "Favorited",
fill: "bg-amber-400",
ring: "focus-visible:ring-amber-400/60 group-hover:border-amber-400/70",
text: "text-amber-500",
darkText: "dark:text-amber-300",
},
{
id: "save",
name: "bookmark",
label: "Save for later",
activeLabel: "Saved",
fill: "bg-indigo-500",
ring: "focus-visible:ring-indigo-500/60 group-hover:border-indigo-500/70",
text: "text-indigo-600",
darkText: "dark:text-indigo-400",
},
{
id: "subscribe",
name: "bell",
label: "Turn on alerts",
activeLabel: "Alerts on",
fill: "bg-emerald-500",
ring: "focus-visible:ring-emerald-500/60 group-hover:border-emerald-500/70",
text: "text-emerald-600",
darkText: "dark:text-emerald-400",
},
{
id: "share",
name: "share",
label: "Share",
activeLabel: "Shared",
fill: "bg-sky-500",
ring: "focus-visible:ring-sky-500/60 group-hover:border-sky-500/70",
text: "text-sky-600",
darkText: "dark:text-sky-400",
},
{
id: "download",
name: "download",
label: "Download file",
activeLabel: "Downloaded",
fill: "bg-violet-500",
ring: "focus-visible:ring-violet-500/60 group-hover:border-violet-500/70",
text: "text-violet-600",
darkText: "dark:text-violet-400",
},
{
id: "play",
name: "play",
label: "Play preview",
activeLabel: "Playing",
fill: "bg-emerald-500",
ring: "focus-visible:ring-emerald-500/60 group-hover:border-emerald-500/70",
text: "text-emerald-600",
darkText: "dark:text-emerald-400",
},
{
id: "copy",
name: "link",
label: "Copy link",
activeLabel: "Copied",
fill: "bg-sky-500",
ring: "focus-visible:ring-sky-500/60 group-hover:border-sky-500/70",
text: "text-sky-600",
darkText: "dark:text-sky-400",
},
];
export default function HoverIconFill() {
const [active, setActive] = useState<Record<string, boolean>>({});
const toggle = (id: string) =>
setActive((prev) => ({ ...prev, [id]: !prev[id] }));
return (
<section className="relative w-full bg-white px-6 py-20 text-slate-900 sm:px-10 dark:bg-slate-950 dark:text-slate-100">
<style>{`
@keyframes hif-pop {
0% { transform: scale(1); }
40% { transform: scale(0.86); }
70% { transform: scale(1.12); }
100% { transform: scale(1); }
}
.hif-pop { animation: hif-pop 360ms cubic-bezier(0.34, 1.56, 0.64, 1); }
@media (prefers-reduced-motion: reduce) {
.hif-pop { animation: none !important; }
.hif-fill, .hif-icon, .hif-tile { transition: none !important; }
}
`}</style>
<div className="mx-auto max-w-5xl">
<div className="mb-14 max-w-2xl">
<span className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-slate-50 px-3 py-1 text-xs font-medium tracking-wide text-slate-600 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-400">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
Interactive
</span>
<h2 className="mt-5 text-3xl font-semibold tracking-tight sm:text-4xl">
Icon buttons that fill on hover
</h2>
<p className="mt-3 text-base leading-relaxed text-slate-600 dark:text-slate-400">
Hover to watch the color sweep in and the glyph flip to white. Click
to commit the action — the fill locks, the icon pops, and the label
updates. Fully keyboard and screen-reader friendly.
</p>
</div>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
{BUTTONS.map((btn) => {
const Icon = ICONS[btn.name];
const isOn = !!active[btn.id];
return (
<button
key={btn.id}
type="button"
onClick={() => toggle(btn.id)}
aria-pressed={isOn}
aria-label={isOn ? btn.activeLabel : btn.label}
className={`hif-tile group relative flex flex-col items-center gap-3 overflow-hidden rounded-2xl border border-slate-200 bg-white px-4 py-6 text-center outline-none transition-colors duration-300 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-slate-800 dark:bg-slate-900 dark:focus-visible:ring-offset-slate-950 ${btn.ring}`}
>
<span
className={`hif-fill pointer-events-none absolute inset-0 origin-bottom scale-y-0 rounded-2xl transition-transform duration-300 ease-out group-hover:scale-y-100 ${btn.fill} ${
isOn ? "scale-y-100" : ""
}`}
aria-hidden="true"
/>
<span
className={`hif-icon relative flex h-12 w-12 items-center justify-center rounded-xl transition-colors duration-300 ${
isOn
? "text-white"
: `${btn.text} ${btn.darkText} group-hover:text-white`
}`}
>
<Icon
className={`h-6 w-6 transition-transform duration-300 group-hover:scale-110 ${
isOn ? "hif-pop" : ""
}`}
/>
</span>
<span
className={`relative text-sm font-medium transition-colors duration-300 ${
isOn
? "text-white"
: "text-slate-700 group-hover:text-white dark:text-slate-300"
}`}
>
{isOn ? btn.activeLabel : btn.label}
</span>
</button>
);
})}
</div>
<p className="mt-10 text-sm text-slate-500 dark:text-slate-500">
Tip: focus a button with Tab and press Enter or Space — the fill and
state toggle exactly as on click.
</p>
</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 →
Card Lift Hover Effect
OriginalA card that lifts with a growing shadow on hover.

Card Glow Hover Effect
OriginalA card with a coloured glow that blooms on hover.

Card Border Draw Hover Effect
OriginalA card whose border draws itself in on hover.

Card Gradient Shift Hover Effect
OriginalA card whose gradient background shifts on hover.

Card Tilt Hover Effect
OriginalA card that tilts in 3D toward the cursor on hover.

Card Spotlight Hover Effect
OriginalA card with a cursor-following radial spotlight.

Link Underline Hover Effect
OriginalAnimated link underline effects: grow, slide and centre-out.

Link Slide Hover Effect
OriginalLinks whose label slides up to a duplicate on hover.

Button Fill Hover Effect
OriginalButtons with a colour fill that sweeps across on hover.

Button Shine Hover Effect
OriginalButtons with a diagonal shine sweep on hover.
Icon Bounce Hover Effect
OriginalIcons that bounce or spin playfully on hover.

List Highlight Hover Effect
OriginalA list where the hovered row gets a sliding highlight and arrow.

