magnetic-cursor
Курсор·Шаблон: Webfolio - Creative Agency & Portfolio Next.js Template·Складність анімації: medium·Адаптивний: Ні
Файли-джерела
- out/home-main/index.html
div.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: differencemakes 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-thiswrapper.
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-thiselements 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.