Magnetic Button Hover Effect

Create an interactive button that follows the cursor movement with magnetic attraction effect. The button should smoothly move towards the cursor when hovering nearby, with elastic easing and optional ripple effect on click.


Magnetic Button Hover Effect


Create an engaging user experience with Magnetic Buttons that playfully track the cursor. This effect adds a premium, interactive feel to your calls to action. The button smoothly follows your mouse movement, creating a sense of weight and physical presence.

HTML Structure

We’ll use a semantic button structure. The text is wrapped in a span to allow for independent parallax movement if desired.

<div class="container">
  <!-- Variant 1: Standard Magnetic -->
  <button class="magnetic-btn">
    <span class="text">Hover Me</span>
  </button>

  <!-- Variant 2: High Intensity -->
  <button class="magnetic-btn intense">
    <span class="text">High Intensity</span>
  </button>
</div>

CSS Styling

The grid layout centers the buttons. The .magnetic-btn class handles the base styling, gradients, and the essential transition for the smooth return effect when the mouse leaves.

:root {
  --primary-bg: #1f1f24;
  --btn-bg-gradient: linear-gradient(180deg, rgba(167, 139, 250, 0.9) 0%, rgba(139, 92, 246, 0.9) 100%);
  --btn-glow-color: rgba(139, 92, 246, 0.6);
  --text-white: #ffffff;
}

body {
  background-color: var(--primary-bg);
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 300px;
  font-family: 'Inter', system-ui, sans-serif;
}

.container {
  display: flex;
  gap: 2rem;
  flex-wrap: wrap;
  justify-content: center;
}

/* Button Base Styles */
.magnetic-btn {
  position: relative;
  padding: 1.25rem 3rem;
  font-size: 1.125rem;
  font-weight: 600;
  color: var(--text-white);
  border: none;
  border-radius: 9999px; /* Pill shape */
  cursor: pointer;
  
  /* Complex Gradient Background */
  background: 
    linear-gradient(180deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0) 100%),
    linear-gradient(90deg, #9333ea, #7c3aed);
  background-blend-mode: overlay, normal;
  
  /* Inner shadows for depth */
  box-shadow: 
    inset 0 2px 4px rgba(255, 255, 255, 0.3), /* Top highlight */
    inset 0 -2px 4px rgba(0, 0, 0, 0.2),      /* Bottom shadow */
    0 0 20px rgba(124, 58, 237, 0.5);         /* Outer Glow */

  /* Border Handling */
  border: 1px solid rgba(255, 255, 255, 0.1);

  /* Performance & Physics */
  transform: translate(0, 0); 
  transition: transform 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94), 
              box-shadow 0.3s ease,
              background 0.3s ease;
  will-change: transform;
  outline: none;
  user-select: none;
  overflow: hidden;
}

/* Hover State */
.magnetic-btn:hover {
  background: 
    linear-gradient(180deg, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0) 100%),
    linear-gradient(90deg, #a855f7, #8b5cf6);
    
  box-shadow: 
    inset 0 2px 4px rgba(255, 255, 255, 0.4), 
    inset 0 -2px 4px rgba(0, 0, 0, 0.2),
    0 0 35px rgba(124, 58, 237, 0.7), /* Stronger glow */
    0 0 10px rgba(139, 92, 246, 0.4);
}

/* Text Element */
.text {
  position: relative;
  z-index: 10;
  pointer-events: none;
  display: inline-block;
  text-shadow: 0 2px 4px rgba(0,0,0,0.2);
  transition: transform 0.1s ease;
  letter-spacing: 0.5px;
}

/* Intense Variant */
.magnetic-btn.intense {
    background: 
    linear-gradient(180deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0) 100%),
    linear-gradient(135deg, #ec4899, #8b5cf6);
    
    box-shadow: 
    inset 0 2px 4px rgba(255, 255, 255, 0.3),
    inset 0 -2px 4px rgba(0, 0, 0, 0.2),
    0 0 20px rgba(236, 72, 153, 0.5);
}

.magnetic-btn.intense:hover {
    box-shadow: 
    inset 0 2px 4px rgba(255, 255, 255, 0.4),
    inset 0 -2px 4px rgba(0, 0, 0, 0.2),
    0 0 40px rgba(236, 72, 153, 0.7);
}


/* Ripple Animation */
.ripple {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%) scale(0);
  width: 150%;
  padding-bottom: 150%;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(255, 255, 255, 0.4) 0%, rgba(255, 255, 255, 0) 70%);
  animation: ripple-effect 0.6s ease-out;
  pointer-events: none;
  z-index: 5;
  mix-blend-mode: overlay;
}

@keyframes ripple-effect {
  to {
      transform: translate(-50%, -50%) scale(2);
      opacity: 0;
  }
}

JavaScript Implementation

The JavaScript handles the physics simulation. It calculates the distance between the cursor and the button’s center, then moves the button a fraction of that distance to create the “magnetic” pull.

/* Select all buttons with the magnetic class */
const buttons = document.querySelectorAll('.magnetic-btn');

buttons.forEach(btn => {
  // Magnetic Effect
  btn.addEventListener('mousemove', function(e) {
    const rect = btn.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;
    
    // Calculate center of button
    const centerX = rect.width / 2;
    const centerY = rect.height / 2;
    
    // Get distance from center
    const deltaX = x - centerX;
    const deltaY = y - centerY;
    
    // Magnetic Intensity:
    // Higher number = Weaker pull (Standard: 5, Intense: 2.5)
    let intensity = 5;
    if (btn.classList.contains('intense')) {
        intensity = 2.5;
    }

    const moveX = deltaX / intensity;
    const moveY = deltaY / intensity;
    
    // Apply transform to button
    btn.style.transform = `translate(${moveX}px, ${moveY}px)`;
    
    // Parallax effect for text (moves slightly less than button for depth)
    const text = btn.querySelector('.text');
    if (text) {
        text.style.transform = `translate(${moveX * 0.2}px, ${moveY * 0.2}px)`;
    }
  });

  // Reset on Mouse Leave
  btn.addEventListener('mouseleave', function() {
    // Determine easing based on variant
    const ease = btn.classList.contains('intense') ? 
                 'cubic-bezier(0.175, 0.885, 0.32, 1.275)' : // Elastic
                 'ease-out';
                 
    btn.style.transition = `transform 0.4s ${ease}`;
    btn.style.transform = 'translate(0, 0)';
    
    const text = btn.querySelector('.text');
    if (text) {
        text.style.transition = `transform 0.4s ${ease}`;
        text.style.transform = 'translate(0, 0)';
    }
    
    // Remove transition inline style after animation to prevent interference with hover re-entry
    setTimeout(() => {
        btn.style.transition = '';
        if(text) text.style.transition = '';
    }, 400);
  });

  // Ripple Effect on Click
  btn.addEventListener('click', function(e) {
    // Create ripple element
    const ripple = document.createElement('span');
    ripple.classList.add('ripple');
    
    // Position ripple at click coordinates relative to button
    const rect = btn.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;
    
    // Since ripple is absolutely positioned, we set left/top
    // However, our CSS centers it with translate(-50%, -50%)
    // So we just need to set left/top to the click position.
    ripple.style.left = `${x}px`;
    ripple.style.top = `${y}px`;
    
    // Add to DOM
    this.appendChild(ripple);
    
    // Clean up
    setTimeout(() => {
      ripple.remove();
    }, 600);
  });
});