custom-magnetic-cursor

Файли-джерела
- home-1.html
div.mil-ball
Бібліотеки
gsapjquery
Summary
A single 20×20 black ball that follows the pointer with a 0.6s sine ease and morphs based on what's hovered: a 90×90 circle with embedded SVG arrow over .mil-drag zones, a More text label over .mil-more, a Choose label over .mil-choose, an accent-orange ball over .mil-accent-cursor, and a near-invisible state over plain links and inputs. Hidden below 1200px viewport.
HTML structure (minimal)
<div class="mil-ball">
<span class="mil-icon-1">
<svg viewBox="0 0 128 128">
<path d="M106.1,41.9c-1.2-1.2-3.1-1.2-4.2,0c-1.2,1.2-1.2,3.1,0,4.2L116.8,61H11.2…" />
</svg>
</span>
<div class="mil-more-text">More</div>
<div class="mil-choose-text">Сhoose</div>
</div>
Key SCSS tokens
.mil-ball {
width: 20px; height: 20px;
position: fixed; z-index: 10;
background-color: $dark; border-radius: 50%;
pointer-events: none; opacity: .1;
display: flex; justify-content: center; align-items: center;
& .mil-icon-1 {
position: absolute;
width: 40px; height: 40px;
transform: scale(0);
& svg { fill: $lt-90; }
}
& .mil-more-text, & .mil-choose-text {
position: absolute; width: 100%; text-align: center;
color: $lt-90; font-size: 12px; font-weight: 500;
text-transform: uppercase; letter-spacing: 2px;
transform: scale(0);
}
&.mil-accent {
& .mil-icon-1 svg { fill: $dark; }
& .mil-more-text, & .mil-choose-text { color: $dark; }
}
@media screen and (max-width: 1200px) { display: none; }
}
Animation logic
const cursor = document.querySelector('.mil-ball');
gsap.set(cursor, { xPercent: -50, yPercent: -50 });
document.addEventListener('pointermove', (e) => {
gsap.to(cursor, { duration: 0.6, ease: 'sine', x: e.clientX, y: e.clientY });
});
// growth on draggable / call-to-action zones
$('.mil-drag, .mil-more, .mil-choose').mouseover(() => {
gsap.to(cursor, .2, { width: 90, height: 90, opacity: 1, ease: 'sine' });
});
$('.mil-drag, .mil-more, .mil-choose').mouseleave(() => {
gsap.to(cursor, .2, { width: 20, height: 20, opacity: .1, ease: 'sine' });
});
// accent flip on `.mil-accent-cursor`
$('.mil-accent-cursor').mouseover(() => {
gsap.to(cursor, .2, { background: accent, ease: 'sine' });
$(cursor).addClass('mil-accent');
});
// per-target inner reveal
$('.mil-drag').mouseover(() => gsap.to('.mil-ball .mil-icon-1', .2, { scale: 1, ease: 'sine' }));
$('.mil-more').mouseover(() => gsap.to('.mil-ball .mil-more-text', .2, { scale: 1, ease: 'sine' }));
$('.mil-choose').mouseover(() => gsap.to('.mil-ball .mil-choose-text', .2, { scale: 1, ease: 'sine' }));
// shrink near regular links and inputs
$('a:not(".mil-choose , .mil-more , .mil-drag , .mil-accent-cursor"), input, textarea, .mil-accordion-menu')
.mouseover(() => gsap.to(cursor, .2, { scale: 0, ease: 'sine' }));
// click feedback
$('body').mousedown(() => gsap.to(cursor, .2, { scale: .1, ease: 'sine' }));
$('body').mouseup(() => gsap.to(cursor, .2, { scale: 1, ease: 'sine' }));
Notable details
- One DOM element handles five behaviours via class targets (.mil-drag → arrow, .mil-more → "More", .mil-choose → "Choose", .mil-accent-cursor → orange, generic link → hide).
- The cursor lags the pointer at 0.6s — long enough to feel "physical" without becoming sluggish; the inner-icon transitions are 0.2s for snappy reads.
- Color invert on dark sections is handled by parent
mi-invert-fixplusmil-accentclass — no per-section overrides on the cursor itself. pointer-events: nonekeeps the cursor non-interactive — clicks always reach the underlying element.
Use when
- Portfolios or agencies that need cursor states tied to content semantics ("drag me", "choose this", "view more") rather than just hover-on-link.
- When you want one shared cursor across multiple page types without per-section bespoke logic.
- Sites that already use GSAP elsewhere — this adds little weight on top.
Caveats
- Hidden entirely below 1200px (mobile/tablet still see the system cursor) — design without relying on the cursor labels for affordance.
- Heavy use of jQuery
.mouseoverdelegation; on dynamically-loaded content (Swup transitions) the script re-binds through theswup:contentReplacedhandler. pointer-events: noneball is positioned viatransform: translate3d— on Firefox, fast pointer movements can leave a brief trail. Acceptable for the target audience.