Nahorniak Templates
База референсів шаблонів і компонентів
Назад до шаблону · Webfolio - Creative Agency & Portfolio Next.js Template
c241

magnetic-cursor

Курсор·Шаблон: Webfolio - Creative Agency & Portfolio Next.js Template·Складність анімації: medium·Адаптивний: Ні

Файли-джерела

  • out/home-main/index.htmldiv.cursor

Summary

A single .cursor div tracks the mouse globally; every .hover-this element pulls its inner .hover-anim toward the pointer (clamped to ±25px) on mousemove and snaps back on mouseleave. The cursor itself adds a cursor-active class when over any <a> or .cursor-pointer, which the CSS uses to grow / invert the dot.

HTML structure (minimal)

<div class="cursor"></div>

<!-- Anywhere on the page -->
<button class="butn butn-sm cursor-pointer">Buy</button>
<a href="/page-contact" class="hover-this">
  <span class="hover-anim">Contact</span>
</a>

Key SCSS tokens

.cursor {
  position: fixed;
  top: 0; left: 0;
  width: 12px; height: 12px;
  margin: -6px 0 0 -6px;
  background: #fff;
  border-radius: 50%;
  pointer-events: none;
  mix-blend-mode: difference;
  z-index: 9999;
  transition: transform .25s ease, background .25s ease;
}
.cursor.cursor-active {
  transform: scale(3);
  background: $main_color; /* #fd5b38 */
}
.hover-this { cursor: pointer; }
.hover-anim { transition: transform .25s ease; will-change: transform; }

Animation logic

// components/common/cusor.jsx
const link = document.querySelectorAll('.hover-this');
const cursor = document.querySelector('.cursor');

const animateit = function (e) {
  const hoverAnim = this.querySelector('.hover-anim');
  const { offsetX: x, offsetY: y } = e;
  const { offsetWidth: w, offsetHeight: h } = this;
  const move = 25;
  const xMove = (x / w) * (move * 2) - move;
  const yMove = (y / h) * (move * 2) - move;
  hoverAnim.style.transform = `translate(${xMove}px, ${yMove}px)`;
  if (e.type === 'mouseleave') hoverAnim.style.transform = '';
};

const editCursor = (e) => {
  cursor.style.left = e.clientX + 'px';
  cursor.style.top  = e.clientY + 'px';
};

link.forEach(b => b.addEventListener('mousemove', animateit));
link.forEach(b => b.addEventListener('mouseleave', animateit));
window.addEventListener('mousemove', editCursor);

document.querySelectorAll('a, .cursor-pointer').forEach(el => {
  el.addEventListener('mousemove', () => cursor.classList.add('cursor-active'));
  el.addEventListener('mouseleave', () => cursor.classList.remove('cursor-active'));
});

Notable details

  • Magnetic effect is per-element, not global — cleaner than rerunning the math for every visible target on every frame.
  • mix-blend-mode: difference makes the white dot legible over both dark and accent-coloured surfaces.
  • Combined with rolling-text, gives interactive labels both a magnetic shift and a per-letter slide on hover.

Use when

  • Portfolio / agency sites that want a cursor flourish without GSAP / kursor / etc.
  • Any layout where you can mark CTAs with a single .hover-this wrapper.

Caveats

  • No touch/pointer-coarse fallback — cursor still renders on mobile but never moves. The CSS hides it via media query in _cursor.scss (@media (max-width: 991px) { .cursor { display: none } }) — verify before deploying.
  • Listeners attach once on mount; dynamically-injected .hover-this elements after first render are ignored.
  • Selector at 1440x900 is a 12x12 dot in the corner — screenshot will be ~empty; rely on the palette-gradient placeholder.