Web InnoventixFreeCode

Icon Bounce Hover Effect

Original · free

Icons that bounce or spin playfully on hover.

byWeb InnoventixReact + Tailwind
hovericonbounce
Open

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-bounce.json
hover-icon-bounce.tsx
"use client";

import { useState, type ReactNode } from "react";

type PlayfulIcon = {
  key: string;
  label: string;
  caption: string;
  motion: "bounce" | "spin" | "wobble" | "pop" | "swing";
  accent: string;
  svg: ReactNode;
};

const ICONS: PlayfulIcon[] = [
  {
    key: "heart",
    label: "Like",
    caption: "Give it some love",
    motion: "bounce",
    accent: "rose",
    svg: (
      <path
        d="M12 20.5S3.5 14.9 3.5 9.3A4.3 4.3 0 0 1 12 7.4a4.3 4.3 0 0 1 8.5 1.9c0 5.6-8.5 11.2-8.5 11.2Z"
        fill="currentColor"
        stroke="currentColor"
        strokeWidth="1.5"
        strokeLinejoin="round"
      />
    ),
  },
  {
    key: "star",
    label: "Favorite",
    caption: "Mark as favorite",
    motion: "spin",
    accent: "amber",
    svg: (
      <path
        d="m12 3.5 2.6 5.3 5.9.9-4.3 4.1 1 5.8L12 17l-5.2 2.7 1-5.8-4.3-4.1 5.9-.9L12 3.5Z"
        fill="currentColor"
        stroke="currentColor"
        strokeWidth="1.4"
        strokeLinejoin="round"
      />
    ),
  },
  {
    key: "bell",
    label: "Notify",
    caption: "Turn on alerts",
    motion: "swing",
    accent: "indigo",
    svg: (
      <>
        <path
          d="M18 8.5a6 6 0 0 0-12 0c0 6-2 7.5-2 7.5h16s-2-1.5-2-7.5Z"
          fill="currentColor"
          stroke="currentColor"
          strokeWidth="1.5"
          strokeLinejoin="round"
        />
        <path
          d="M10.3 19.5a2 2 0 0 0 3.4 0"
          fill="none"
          stroke="currentColor"
          strokeWidth="1.5"
          strokeLinecap="round"
        />
      </>
    ),
  },
  {
    key: "bolt",
    label: "Boost",
    caption: "Supercharge it",
    motion: "pop",
    accent: "violet",
    svg: (
      <path
        d="M13 2.5 4.5 13.2h6L11 21.5 19.5 10.8h-6L13 2.5Z"
        fill="currentColor"
        stroke="currentColor"
        strokeWidth="1.4"
        strokeLinejoin="round"
      />
    ),
  },
  {
    key: "rocket",
    label: "Launch",
    caption: "Ship it now",
    motion: "bounce",
    accent: "sky",
    svg: (
      <>
        <path
          d="M12 3c3.5 1.5 5.5 5 5.5 8.5L12 15l-5.5-3.5C6.5 8 8.5 4.5 12 3Z"
          fill="currentColor"
          stroke="currentColor"
          strokeWidth="1.4"
          strokeLinejoin="round"
        />
        <path
          d="M9.4 15.2 7 20.5l4-2 4 2-2.4-5.3"
          fill="none"
          stroke="currentColor"
          strokeWidth="1.4"
          strokeLinejoin="round"
          strokeLinecap="round"
        />
        <circle cx="12" cy="9.5" r="1.4" fill="currentColor" />
      </>
    ),
  },
  {
    key: "smile",
    label: "React",
    caption: "Drop a reaction",
    motion: "wobble",
    accent: "emerald",
    svg: (
      <>
        <circle cx="12" cy="12" r="8.5" fill="currentColor" opacity="0.18" />
        <circle
          cx="12"
          cy="12"
          r="8.5"
          fill="none"
          stroke="currentColor"
          strokeWidth="1.5"
        />
        <circle cx="9.2" cy="10.5" r="1.1" fill="currentColor" />
        <circle cx="14.8" cy="10.5" r="1.1" fill="currentColor" />
        <path
          d="M8.5 14a4.5 4.5 0 0 0 7 0"
          fill="none"
          stroke="currentColor"
          strokeWidth="1.5"
          strokeLinecap="round"
        />
      </>
    ),
  },
];

const ACCENTS: Record<
  string,
  { text: string; ring: string; glow: string; chip: string }
> = {
  rose: {
    text: "text-rose-500 dark:text-rose-400",
    ring: "focus-visible:ring-rose-400/70",
    glow: "group-hover:shadow-rose-400/40 dark:group-hover:shadow-rose-500/30",
    chip: "bg-rose-50 text-rose-700 dark:bg-rose-500/10 dark:text-rose-300",
  },
  amber: {
    text: "text-amber-500 dark:text-amber-400",
    ring: "focus-visible:ring-amber-400/70",
    glow: "group-hover:shadow-amber-400/40 dark:group-hover:shadow-amber-500/30",
    chip: "bg-amber-50 text-amber-700 dark:bg-amber-500/10 dark:text-amber-300",
  },
  indigo: {
    text: "text-indigo-500 dark:text-indigo-400",
    ring: "focus-visible:ring-indigo-400/70",
    glow: "group-hover:shadow-indigo-400/40 dark:group-hover:shadow-indigo-500/30",
    chip: "bg-indigo-50 text-indigo-700 dark:bg-indigo-500/10 dark:text-indigo-300",
  },
  violet: {
    text: "text-violet-500 dark:text-violet-400",
    ring: "focus-visible:ring-violet-400/70",
    glow: "group-hover:shadow-violet-400/40 dark:group-hover:shadow-violet-500/30",
    chip: "bg-violet-50 text-violet-700 dark:bg-violet-500/10 dark:text-violet-300",
  },
  sky: {
    text: "text-sky-500 dark:text-sky-400",
    ring: "focus-visible:ring-sky-400/70",
    glow: "group-hover:shadow-sky-400/40 dark:group-hover:shadow-sky-500/30",
    chip: "bg-sky-50 text-sky-700 dark:bg-sky-500/10 dark:text-sky-300",
  },
  emerald: {
    text: "text-emerald-500 dark:text-emerald-400",
    ring: "focus-visible:ring-emerald-400/70",
    glow: "group-hover:shadow-emerald-400/40 dark:group-hover:shadow-emerald-500/30",
    chip: "bg-emerald-50 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-300",
  },
};

export default function HoverIconBounce() {
  const [tapped, setTapped] = useState<string | null>(null);

  return (
    <section className="relative w-full overflow-hidden bg-slate-50 px-6 py-20 text-slate-900 dark:bg-slate-950 dark:text-slate-100 sm:px-8 md:py-28">
      <style>{`
        @keyframes hib-bounce {
          0%, 100% { transform: translateY(0) scale(1); }
          30% { transform: translateY(-9px) scale(1.12); }
          55% { transform: translateY(2px) scale(0.96); }
          75% { transform: translateY(-3px) scale(1.03); }
        }
        @keyframes hib-spin {
          0% { transform: rotate(0deg) scale(1); }
          60% { transform: rotate(345deg) scale(1.12); }
          100% { transform: rotate(360deg) scale(1); }
        }
        @keyframes hib-wobble {
          0%, 100% { transform: rotate(0deg); }
          20% { transform: rotate(-13deg) scale(1.08); }
          40% { transform: rotate(10deg) scale(1.08); }
          60% { transform: rotate(-6deg); }
          80% { transform: rotate(3deg); }
        }
        @keyframes hib-pop {
          0% { transform: scale(1); }
          40% { transform: scale(1.28); }
          70% { transform: scale(0.92); }
          100% { transform: scale(1.06); }
        }
        @keyframes hib-swing {
          0%, 100% { transform: rotate(0deg); transform-origin: top center; }
          20% { transform: rotate(16deg); transform-origin: top center; }
          45% { transform: rotate(-12deg); transform-origin: top center; }
          70% { transform: rotate(7deg); transform-origin: top center; }
          88% { transform: rotate(-3deg); transform-origin: top center; }
        }
        @keyframes hib-tap {
          0% { transform: scale(1); }
          45% { transform: scale(1.35); }
          100% { transform: scale(1); }
        }
        @keyframes hib-float-in {
          from { opacity: 0; transform: translateY(10px); }
          to { opacity: 1; transform: translateY(0); }
        }

        .hib-card { will-change: transform; }
        .hib-icon { will-change: transform; }

        .hib-card:hover .hib-icon[data-motion="bounce"],
        .hib-card:focus-within .hib-icon[data-motion="bounce"] {
          animation: hib-bounce 0.7s cubic-bezier(0.28, 0.84, 0.42, 1) both;
        }
        .hib-card:hover .hib-icon[data-motion="spin"],
        .hib-card:focus-within .hib-icon[data-motion="spin"] {
          animation: hib-spin 0.7s cubic-bezier(0.34, 1.4, 0.5, 1) both;
        }
        .hib-card:hover .hib-icon[data-motion="wobble"],
        .hib-card:focus-within .hib-icon[data-motion="wobble"] {
          animation: hib-wobble 0.7s ease-in-out both;
        }
        .hib-card:hover .hib-icon[data-motion="pop"],
        .hib-card:focus-within .hib-icon[data-motion="pop"] {
          animation: hib-pop 0.55s cubic-bezier(0.34, 1.56, 0.5, 1) both;
        }
        .hib-card:hover .hib-icon[data-motion="swing"],
        .hib-card:focus-within .hib-icon[data-motion="swing"] {
          animation: hib-swing 0.8s ease-in-out both;
        }

        .hib-icon[data-tapped="true"] {
          animation: hib-tap 0.4s cubic-bezier(0.34, 1.56, 0.5, 1) both !important;
        }

        @media (prefers-reduced-motion: reduce) {
          .hib-card:hover .hib-icon,
          .hib-card:focus-within .hib-icon,
          .hib-icon[data-tapped="true"] {
            animation: none !important;
          }
          .hib-card:hover .hib-icon,
          .hib-card:focus-within .hib-icon {
            transform: scale(1.08);
          }
          .hib-float { animation: none !important; }
        }
      `}</style>

      <div className="pointer-events-none absolute -left-24 top-0 h-72 w-72 rounded-full bg-indigo-300/20 blur-3xl dark:bg-indigo-500/10" />
      <div className="pointer-events-none absolute -right-16 bottom-0 h-72 w-72 rounded-full bg-rose-300/20 blur-3xl dark:bg-rose-500/10" />

      <div className="relative mx-auto max-w-5xl">
        <div className="hib-float mx-auto max-w-2xl text-center" style={{ animation: "hib-float-in 0.5s ease-out both" }}>
          <span className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white 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" />
            Hover · Interactive
          </span>
          <h2 className="mt-5 text-3xl font-semibold tracking-tight sm:text-4xl">
            Icons with a little bounce
          </h2>
          <p className="mt-3 text-base leading-relaxed text-slate-600 dark:text-slate-400">
            Hover, tab, or tap each button. Every icon has its own playful
            reaction — a bounce, a spin, a swing — and it all goes still if you
            prefer reduced motion.
          </p>
        </div>

        <ul className="mt-12 grid grid-cols-2 gap-4 sm:grid-cols-3 sm:gap-5">
          {ICONS.map((icon) => {
            const accent = ACCENTS[icon.accent];
            const isTapped = tapped === icon.key;
            return (
              <li key={icon.key}>
                <button
                  type="button"
                  aria-label={`${icon.label} — ${icon.caption}`}
                  onClick={() => {
                    setTapped(icon.key);
                    window.setTimeout(() => {
                      setTapped((cur) => (cur === icon.key ? null : cur));
                    }, 450);
                  }}
                  className={`hib-card group flex w-full flex-col items-center gap-3 rounded-2xl border border-slate-200 bg-white px-4 py-7 text-center shadow-sm outline-none transition-all duration-300 hover:-translate-y-1 hover:border-slate-300 hover:shadow-lg focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:border-slate-800 dark:bg-slate-900 dark:hover:border-slate-700 dark:focus-visible:ring-offset-slate-950 ${accent.glow} ${accent.ring}`}
                >
                  <span
                    className={`flex h-14 w-14 items-center justify-center rounded-2xl ${accent.chip}`}
                  >
                    <span
                      className={`hib-icon ${accent.text}`}
                      data-motion={icon.motion}
                      data-tapped={isTapped ? "true" : "false"}
                    >
                      <svg
                        viewBox="0 0 24 24"
                        width="30"
                        height="30"
                        aria-hidden="true"
                        focusable="false"
                      >
                        {icon.svg}
                      </svg>
                    </span>
                  </span>
                  <span className="text-sm font-semibold text-slate-900 dark:text-slate-100">
                    {icon.label}
                  </span>
                  <span className="text-xs leading-snug text-slate-500 dark:text-slate-400">
                    {icon.caption}
                  </span>
                </button>
              </li>
            );
          })}
        </ul>

        <p className="mt-8 text-center text-xs text-slate-400 dark:text-slate-500">
          Keyboard-friendly: press Tab to focus a tile and watch it react.
        </p>
      </div>
    </section>
  );
}

Dependencies

None (React + Tailwind only).

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 quote

Similar components

Browse all →