← Back to blog
·6 min read

Icon Hover Animation with CSS: 8 Effects with Copy-Paste Code

TL;DR

  • Eight copy-paste CSS icon hover effects: spring scale, tilt, bell ring, wiggle, bounce, spin, stroke draw and colour sweep
  • Animate only transform and opacity, trigger from the button, and pair :hover with :focus-visible
  • transform-origin and an overshooting cubic-bezier make simple effects feel springy
  • For icons that change shape or state, see the animated icons collection

A good icon hover animation is small, fast and a little bit playful. This guide gives you eight CSS icon hover effects you can copy into any project, plain HTML or React, plus the rules that keep them smooth and accessible.

Every example assumes an icon inside a button:

<button class="icon-btn" aria-label="Notifications">
  <svg class="icon" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true">…</svg>
</button>

The base rules

.icon-btn .icon {
  transition: transform 220ms cubic-bezier(0.34, 1.56, 0.64, 1);
  transform-origin: center;
}
@media (prefers-reduced-motion: reduce) {
  .icon-btn .icon,
  .icon-btn .icon * { animation: none !important; transition: none !important; }
}
  • Animate only transform and opacity. They run on the compositor, so they stay at 60fps.
  • Trigger on the button, not the icon: .icon-btn:hover .icon. The hit area is bigger and keyboard focus can share the selector.
  • Always pair :hover with :focus-visible.
  • 1. Spring scale

    .icon-btn:hover .icon,
    .icon-btn:focus-visible .icon { transform: scale(1.18); }

    The overshooting curve in the base rule makes it pop rather than grow.

    2. Tilt

    .icon-btn:hover .icon,
    .icon-btn:focus-visible .icon { transform: rotate(-12deg) scale(1.08); }

    Good for pencils, search icons and arrows.

    3. Bell ring

    @keyframes ring {
      0%, 100% { transform: rotate(0); }
      20% { transform: rotate(14deg); }
      40% { transform: rotate(-12deg); }
      60% { transform: rotate(8deg); }
      80% { transform: rotate(-4deg); }
    }
    .icon-btn.bell .icon { transform-origin: 50% 10%; }
    .icon-btn.bell:hover .icon,
    .icon-btn.bell:focus-visible .icon { animation: ring 600ms ease-in-out; }

    Moving transform-origin to the top makes the bell swing from its hook. For a bell that also toggles muted and unmuted with a slash drawing in, see the Mute Bell Toggle.

    4. Wiggle

    @keyframes wiggle {
      0%, 100% { transform: translateX(0); }
      25% { transform: translateX(-2px) rotate(-4deg); }
      75% { transform: translateX(2px) rotate(4deg); }
    }
    .icon-btn:hover .icon { animation: wiggle 300ms ease-in-out 2; }

    5. Bounce up

    @keyframes bounce-up {
      0%, 100% { transform: translateY(0); }
      40% { transform: translateY(-5px); }
      70% { transform: translateY(1px); }
    }
    .icon-btn:hover .icon { animation: bounce-up 450ms cubic-bezier(0.34, 1.56, 0.64, 1); }

    6. Spin

    .icon-btn.refresh .icon { transition: transform 600ms cubic-bezier(0.65, 0, 0.35, 1); }
    .icon-btn.refresh:hover .icon { transform: rotate(360deg); }

    7. Stroke draw-on

    For line icons (Lucide, Feather, Heroicons outline), redraw the stroke on hover:

    .icon-btn .icon path {
      stroke-dasharray: 1;
      stroke-dashoffset: 0;
    }
    .icon-btn:hover .icon path { animation: draw 500ms ease-out; }
    @keyframes draw {
      from { stroke-dashoffset: 1; }
      to { stroke-dashoffset: 0; }
    }

    Add pathLength="1" to each <path> so 1 means the full length of every path. More SVG techniques are in animated SVG icons.

    8. Colour fill sweep

    .icon-btn .icon { color: #8a8a8a; transition: color 160ms ease, transform 220ms cubic-bezier(0.34, 1.56, 0.64, 1); }
    .icon-btn:hover .icon { color: #ff5a36; transform: scale(1.08); }

    Icons that use stroke="currentColor" or fill="currentColor" follow the button's color, so one property recolours the whole icon.

    Doing it in React

    With React the CSS stays the same; wrap it in a component and pass the effect as a class:

    export function IconButton({ label, effect = 'scale', children }: {
      label: string
      effect?: 'scale' | 'tilt' | 'bell' | 'refresh'
      children: React.ReactNode
    }) {
      return (
        <button aria-label={label} className={`icon-btn ${effect}`}>
          {children}
        </button>
      )
    }

    To animate Lucide icons specifically, see how to animate Lucide icons in React.

    When CSS is not enough

    CSS hover effects cannot change an icon's shape, hold multiple states, or react to where the pointer is. For hover icons with real squash-and-stretch, nav icons with a different move each, and toggles, use an interactive format such as Rive. The animated icons collection has those ready to paste into Cursor, Claude Code, Lovable or v0.