30 CSS Hover Effects 30 / 30

CSS Magnetic Cursor Pull Hover Effect

Buttons and icons that magnetically attract to the cursor when nearby, snapping back smoothly when the cursor leaves — four magnetic element types.

CSS + JS MIT licensed
Live Demo Open in tab

This is a full-page demo — interact inside the frame above, or open it in the playground for the full-screen experience.

Open in playground

The code

<div class="hv-30">
  <p class="hv-30__label">Magnetic Cursor Pull — 4 Types</p>
  <div class="hv-30__grid">

    <div class="hv-30__wrap">
      <button class="hv-30__btn hv-30__btn--primary hv-30__magnet" data-strength="0.35" data-radius="80">
        <span class="hv-30__btn-inner hv-30__magnet-inner" data-strength="0.55">Get Started</span>
      </button>
      <span class="hv-30__caption">Primary CTA</span>
    </div>

    <div class="hv-30__wrap">
      <button class="hv-30__btn hv-30__btn--outline hv-30__magnet" data-strength="0.4" data-radius="90">
        <span class="hv-30__btn-inner hv-30__magnet-inner" data-strength="0.6">Learn More</span>
      </button>
      <span class="hv-30__caption">Outline Button</span>
    </div>

    <div class="hv-30__wrap">
      <div class="hv-30__icon-btn hv-30__magnet" data-strength="0.5" data-radius="70">
        <span class="hv-30__magnet-inner" data-strength="0.75">★</span>
      </div>
      <span class="hv-30__caption">Icon Pull</span>
    </div>

    <div class="hv-30__wrap">
      <a class="hv-30__pill hv-30__magnet" href="#" data-strength="0.3" data-radius="100">
        <span class="hv-30__magnet-inner" data-strength="0.5">View Project →</span>
      </a>
      <span class="hv-30__caption">Pill Link</span>
    </div>

  </div>
</div>
.hv-30,
.hv-30 *,
.hv-30 *::before,
.hv-30 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}
.hv-30 {
  font-family: system-ui, sans-serif;
  background: #090912;
  padding: 3rem 2rem;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2rem;
}
.hv-30__label {
  color: #444;
  font-size: .72rem;
  letter-spacing: .15em;
  text-transform: uppercase;
}
.hv-30__grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 3.5rem 4rem;
  place-items: center;
  padding: 2rem;
}
.hv-30__wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
}
.hv-30__caption {
  color: #333;
  font-size: .68rem;
  letter-spacing: .1em;
  text-transform: uppercase;
}

/* Magnetic elements */
.hv-30__magnet {
  will-change: transform;
  transition: transform .45s cubic-bezier(.23,1,.32,1);
  cursor: pointer;
}
.hv-30__magnet-inner {
  display: inline-block;
  will-change: transform;
  transition: transform .45s cubic-bezier(.23,1,.32,1);
  pointer-events: none;
}

/* Button styles */
.hv-30__btn {
  background: #5b4fcf;
  color: #fff;
  border: none;
  border-radius: 50px;
  padding: .9rem 2.4rem;
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: .02em;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 6px 28px rgba(91,79,207,.4);
}
.hv-30__btn--outline {
  background: transparent;
  border: 2px solid #5b4fcf;
  color: #a99ef7;
  box-shadow: none;
}
.hv-30__btn--outline:hover { box-shadow: 0 6px 28px rgba(91,79,207,.25); }

/* Icon button */
.hv-30__icon-btn {
  width: 64px; height: 64px;
  border-radius: 50%;
  background: linear-gradient(135deg, #7c6af7, #f764a8);
  color: #fff;
  font-size: 1.6rem;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 6px 24px rgba(124,106,247,.4);
}

/* Pill link */
.hv-30__pill {
  display: inline-flex;
  align-items: center;
  padding: .65rem 1.8rem;
  background: rgba(255,255,255,.06);
  border: 1px solid rgba(255,255,255,.12);
  border-radius: 50px;
  color: #ccc;
  text-decoration: none;
  font-size: .9rem;
  backdrop-filter: blur(6px);
  transition: transform .45s cubic-bezier(.23,1,.32,1),
              background .3s, border-color .3s;
}
.hv-30__pill:hover {
  background: rgba(255,255,255,.1);
  border-color: rgba(255,255,255,.22);
  color: #fff;
}

@media (max-width: 500px) { .hv-30__grid { grid-template-columns: 1fr; } }
@media (prefers-reduced-motion: reduce) {
  .hv-30__magnet,
  .hv-30__magnet-inner { transition: none !important; }
}
(function () {
  if (window.matchMedia('(pointer: coarse)').matches) return;

  const magnets = document.querySelectorAll('.hv-30__magnet');

  function handleMove(e, el) {
    const rect = el.getBoundingClientRect();
    const cx = rect.left + rect.width  / 2;
    const cy = rect.top  + rect.height / 2;
    const dx = e.clientX - cx;
    const dy = e.clientY - cy;
    const dist = Math.sqrt(dx * dx + dy * dy);
    const radius   = parseFloat(el.dataset.radius)   || 80;
    const strength = parseFloat(el.dataset.strength)  || 0.35;

    if (dist < radius) {
      const pull = (1 - dist / radius);
      const tx = dx * strength * pull;
      const ty = dy * strength * pull;
      el.style.transform = `translate(${tx}px, ${ty}px)`;

      const inner = el.querySelector('.hv-30__magnet-inner');
      if (inner) {
        const is = parseFloat(inner.dataset.strength) || 0.5;
        inner.style.transform = `translate(${dx * is * pull}px, ${dy * is * pull}px)`;
      }
    } else {
      el.style.transform = '';
      const inner = el.querySelector('.hv-30__magnet-inner');
      if (inner) inner.style.transform = '';
    }
  }

  function handleLeave(el) {
    el.style.transform = '';
    const inner = el.querySelector('.hv-30__magnet-inner');
    if (inner) inner.style.transform = '';
  }

  magnets.forEach(el => {
    document.addEventListener('mousemove', e => handleMove(e, el));
    el.addEventListener('mouseleave', () => handleLeave(el));
  });
})();

How this works

JavaScript calculates the distance from the cursor to each element's center. When within a configurable radius, it applies a CSS transform: translate() proportional to the offset (strength factor 0–1). On mouse-leave, CSS transition springs the element back to origin. The inner label can pull at a different strength than the button border for a parallax layer effect.

Customize

  • Change data-strength (0.1–0.6) on each element for pull intensity, data-radius for the activation distance in px, or apply to SVG icons for icon-specific magnetic pulls.

Watch out for

  • Magnetic pull is calculated in viewport coordinates but applied as a local translate — always subtract the element's getBoundingClientRect center, not its offsetLeft.
  • Use will-change: transform sparingly — it promotes the element to its own compositor layer which is expensive if there are many magnetic elements.
  • Disable on touch devices with a pointer:coarse media query or navigator.maxTouchPoints check to prevent sticky elements.

Browser support

ChromeSafariFirefoxEdge

Uses standard DOM events and CSS transforms — fully cross-browser.

Search CodeFronts

Loading…