Morphing SVG Icons

Animated icons that morph between two states—menu ↔ close, play ↔ pause, heart ↔ broken heart. Uses SVG path animations with smooth transitions and interactive click/hover triggers for a modern, sleek UI.


Morphing SVG Icons


Build animated icons that morph smoothly between two states—perfect for navigation menus, media controls, and social interactions. This snippet uses SVG path animations with CSS transitions and vanilla JavaScript for lightweight, performant morphing effects.

Features

HTML Structure

Each icon is a clickable button containing an SVG. Use data-state or a class to track the current state for toggling.

<div class="morph-icons-grid">
  <!-- Menu ↔ Close -->
  <button class="morph-icon" id="menu-icon" aria-label="Toggle menu">
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
      <line class="line line-top" x1="3" y1="6" x2="21" y2="6"/>
      <line class="line line-mid" x1="3" y1="12" x2="21" y2="12"/>
      <line class="line line-bot" x1="3" y1="18" x2="21" y2="18"/>
    </svg>
  </button>

  <!-- Play ↔ Pause -->
  <button class="morph-icon" id="play-icon" aria-label="Play/Pause">
    <svg viewBox="0 0 24 24">
      <path class="play-path" d="M8 5v14l11-7z"/>
      <g class="pause-group">
        <rect class="pause-bar" x="6" y="4" width="4" height="16" rx="1"/>
        <rect class="pause-bar" x="14" y="4" width="4" height="16" rx="1"/>
      </g>
    </svg>
  </button>

  <!-- Heart ↔ Broken Heart -->
  <button class="morph-icon" id="heart-icon" aria-label="Like">
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
      <path class="heart-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 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>
      <path class="broken-path" d="M4.84 4.61a5.5 5.5 0 0 0 0 7.78l1.06 1.06L12 21.23l2.91-2.91-4.5-4.5 2.5-2.5-4.5-4.5-2.5 2.5-2.06-2.06a5.5 5.5 0 0 0-7.78-7.78l1.06 1.06z"/>
    </svg>
  </button>
</div>

CSS: Menu ↔ Close (Hamburger to X)

The hamburger uses three lines. When active, the top and bottom lines rotate and translate to form an X; the middle line fades out.

.morph-icon {
  width: 48px;
  height: 48px;
  padding: 12px;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 12px;
  cursor: pointer;
  transition: background 0.3s, border-color 0.3s;
}

.morph-icon svg {
  width: 100%;
  height: 100%;
  display: block;
}

.morph-icon .line {
  transform-origin: center;
  transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1),
              opacity 0.25s ease;
}

.morph-icon.active .line-top {
  transform: translateY(6px) rotate(45deg);
}

.morph-icon.active .line-mid {
  opacity: 0;
}

.morph-icon.active .line-bot {
  transform: translateY(-6px) rotate(-45deg);
}

CSS: Play ↔ Pause

Use two overlapping groups—play triangle and pause bars—and toggle visibility with opacity and scale.

.morph-icon .play-path {
  transform-origin: center;
  transition: opacity 0.3s, transform 0.3s;
}

.morph-icon .pause-group {
  opacity: 0;
  transform: scale(0.8);
  transform-origin: center;
  transition: opacity 0.3s, transform 0.3s;
  pointer-events: none;
}

.morph-icon.active .play-path {
  opacity: 0;
  transform: scale(0.8);
}

.morph-icon.active .pause-group {
  opacity: 1;
  transform: scale(1);
}

CSS: Heart ↔ Broken Heart

Two paths overlap. Morph by transitioning opacity so one fades in as the other fades out, creating a smooth crossfade effect.

.morph-icon .heart-path,
.morph-icon .broken-path {
  transition: opacity 0.4s ease, stroke 0.3s;
}

.morph-icon .broken-path {
  opacity: 0;
  stroke: #ef4444;
}

.morph-icon.active .heart-path {
  opacity: 0;
}

.morph-icon.active .broken-path {
  opacity: 1;
}

JavaScript: Toggle on Click

Attach click handlers to toggle the active class on each button.

document.querySelectorAll('.morph-icon').forEach(btn => {
  btn.addEventListener('click', () => btn.classList.toggle('active'));
});

For hover-triggered morphing instead of click, use mouseenter and mouseleave:

document.querySelectorAll('.morph-icon').forEach(btn => {
  btn.addEventListener('mouseenter', () => btn.classList.add('active'));
  btn.addEventListener('mouseleave', () => btn.classList.remove('active'));
});

Resources

Tips