Icons Animating Icons with CSS

CSS transitions and @keyframes animations can turn a static icon into a spinner, a pulsing indicator, or a bouncing hint, as long as you also respect prefers-reduced-motion for users who've asked for less motion.

Bringing Icons to Life with CSS

Two CSS tools cover almost every icon animation you'll need. transition animates a property smoothly between two states, like an icon rotating slightly or changing color on hover. animation, paired with @keyframes, plays a defined sequence of steps that can repeat indefinitely, like a loading spinner that never stops turning. The rule of thumb: reach for transition when responding to a one-time state change, and animation for anything continuous or made of multiple steps.

Example

<!DOCTYPE html>
<html>
<head>
</head>
<body>

<button class="star-btn" aria-pressed="false">
  <svg class="star-icon" viewBox="0 0 24 24" width="22" height="22" aria-hidden="true">
    <path d="M12 2l2.9 6.6 7.1.6-5.4 4.7 1.7 7-6.3-3.9L5.7 21l1.7-7-5.4-4.7 7.1-.6L12 2Z" fill="none" stroke="currentColor" stroke-width="1.5"/>
  </svg>
  Favorite
</button>

<style>
.star-icon {
  transition: transform 0.2s ease, color 0.2s ease;
  color: #999;
}
.star-btn:hover .star-icon,
.star-btn:focus-visible .star-icon {
  transform: scale(1.15);
  color: #f5a623;
}
</style>

</body>
</html>

Keyframe Animations: Spin, Pulse, and Bounce

@keyframes defines named waypoints in an animation using percentages, or the from and to shorthand for just the start and end. Each waypoint lists the CSS property values the element should have at that point in the timeline, and the browser smoothly interpolates between them. You then attach the keyframes to an element with the animation shorthand, which sets the animation's name, duration, timing function, and how many times it repeats.

Example

<!DOCTYPE html>
<html>
<head>
</head>
<body>

<div class="icon-row">
  <svg class="icon spin" viewBox="0 0 24 24" width="28" height="28" aria-hidden="true">
    <path d="M12 3a9 9 0 1 0 9 9" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
  </svg>
  <svg class="icon pulse" viewBox="0 0 24 24" width="28" height="28" aria-hidden="true">
    <circle cx="12" cy="12" r="8" fill="currentColor"/>
  </svg>
  <svg class="icon bounce" viewBox="0 0 24 24" width="28" height="28" aria-hidden="true">
    <path d="M12 4v12m0 0-4-4m4 4 4-4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
  </svg>
</div>

<style>
.icon { color: #4a67f5; }

@keyframes spin {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}
.spin {
  animation: spin 1s linear infinite;
}

@keyframes pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50%      { opacity: 0.4; transform: scale(0.85); }
}
.pulse {
  animation: pulse 1.4s ease-in-out infinite;
}

@keyframes bounce {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-6px); }
}
.bounce {
  animation: bounce 0.8s ease-in-out infinite;
}
</style>

</body>
</html>
PropertyWhat it controls
animation-nameWhich @keyframes rule to play, for example spin.
animation-durationHow long one cycle takes, e.g. 1s or 800ms.
animation-timing-functionThe pacing of the motion: linear for a constant-speed spin, ease-in-out for a smoother pulse or bounce.
animation-iteration-countHow many times it plays, a number, or infinite for loaders.
animation-fill-modeWhether the element keeps the first or last keyframe's styles before it starts or after it ends.
  • Animate transform and opacity where possible, they run on the compositor and stay smooth even on slower devices.
  • Avoid animating properties like width, top, or margin for continuous loops, since they force the browser to recalculate layout on every frame.
  • Keep infinite looping animations, like spinners, reserved for genuine loading states, not decorative flourishes that never stop.
  • Pair a short transition on transform: scale() with hover or focus states so interactive icons feel responsive without needing @keyframes at all.

Respecting prefers-reduced-motion

Some users experience real physical discomfort, such as dizziness, nausea, or headaches, from large or continuous motion, and many have told their operating system so by turning on a "reduce motion" setting. CSS can read that preference through the prefers-reduced-motion media feature. When it matches reduce, scale back or remove non-essential animation so the page stays comfortable to use, while keeping the functional meaning, like "this is loading", intact through some other cue.

Example

<!DOCTYPE html>
<html>
<head>
</head>
<body>

<style>
.spinner {
  animation: spin 1s linear infinite;
}

@media (prefers-reduced-motion: reduce) {
  .spinner {
    animation: none;
  }
  /* A loading spinner still needs to communicate "busy" somehow;
     a subtle opacity fade is far gentler than continuous rotation */
  .spinner {
    animation: fade 1.6s ease-in-out infinite;
  }
}

@keyframes spin {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}

@keyframes fade {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.5; }
}
</style>

</body>
</html>
Note: Test this yourself: on macOS, enable Settings > Accessibility > Display > Reduce motion; on Windows, Settings > Accessibility > Visual effects > Animation effects. Then reload your page, any animation you didn't gate behind prefers-reduced-motion will still play at full strength.

Exercise: Icons Animation

What CSS mechanism is commonly used to make an icon spin continuously, like a loading indicator?