מהו אפקט הכפתור המגנטי ולמה הוא נחוץ?
תארו לעצמכם: המשתמש מזיז את הסמן מעל כפתור, והוא לא מגיב. אין משוב. רכיב ה-CTA מתמזג עם הרקע. ההמרות יורדות. אפקט הכפתור המגנטי פותר את זה: בעת ריחוף, התוכן הפנימי של הכפתור (טקסט, אייקון) זז לכיוון הסמן, בעוד שהמכלול החיצוני זז מעט פחות. זה יוצר תחושה של גמישות ומגע. לפי בדיקות ה-A/B שלנו, האנימציה הזו מגדילה את המעורבות ב-15–20%.
האפקט עובד על שלושה עקרונות: אזור המשיכה רחב יותר מהכפתור עצמו, התנועה מאטה (קפיצית), וכשהסמן עוזב, הוא חוזר למקומו המקורי עם גמישות. זה מושך תשומת לב לרכיבי CTA ומשפר את חוויית המשתמש.
כיצד האפקט המגנטי משפיע על Core Web Vitals?
עם יישום נכון — בכלל לא. הטכניקות בהן משתמשים (will-change) והאצת GPU באמצעות requestAnimationFrame לא יוצרות משימות ארוכות או מגדילות את Cumulative Layout Shift. במכשירים ניידים, האפקט מושבת באמצעות matchMedia('(hover: hover)') כדי למנוע חישובים מיותרים. עם יישום נכון, האפקט לא משפיע על Core Web Vitals.
איך עובדת המתמטיקה של המשיכה?
בכל פריים, מחושב המרחק ממרכז הכפתור לסמן. אם הסמן נמצא באזור המשיכה, ההזזה מחושבת באופן יחסי למרחק ומנורמלת לערך מקסימלי.
interface MagneticConfig {
strength: number // сила притяжения, 0.3–0.6
innerStrength: number // сила для внутреннего контента, 0.6–1.2
radius: number // множитель зоны притяжения от размера кнопки
}
class MagneticButton {
private el: HTMLElement
private inner: HTMLElement
private bounds: DOMRect
private config: MagneticConfig
private rafId: number | null = null
// Текущие смещения (анимированные)
private xEl = 0
private yEl = 0
private xInner = 0
private yInner = 0
// Целевые смещения
private targetXEl = 0
private targetYEl = 0
private targetXInner = 0
private targetYInner = 0
private readonly LERP = 0.15
constructor(el: HTMLElement, config: Partial<MagneticConfig> = {}) {
this.el = el
this.inner = el.querySelector('[data-magnetic-inner]') || el
this.config = {
strength: 0.4,
innerStrength: 0.8,
radius: 1.6,
...config,
}
this.bounds = el.getBoundingClientRect()
this.init()
}
private init() {
window.addEventListener('mousemove', this.onMouseMove)
window.addEventListener('resize', this.recalcBounds)
this.tick()
}
private recalcBounds = () => {
this.bounds = this.el.getBoundingClientRect()
}
private onMouseMove = (e: MouseEvent) => {
const { left, top, width, height } = this.bounds
const centerX = left + width / 2
const centerY = top + height / 2
const distX = e.clientX - centerX
const distY = e.clientY - centerY
const distance = Math.sqrt(distX ** 2 + distY ** 2)
const threshold = (Math.max(width, height) / 2) * this.config.radius
if (distance < threshold) {
const force = (threshold - distance) / threshold
this.targetXEl = distX * this.config.strength * force
this.targetYEl = distY * this.config.strength * force
this.targetXInner = distX * this.config.innerStrength * force
this.targetYInner = distY * this.config.innerStrength * force
} else {
this.targetXEl = 0
this.targetYEl = 0
this.targetXInner = 0
this.targetYInner = 0
}
}
private tick = () => {
this.xEl += (this.targetXEl - this.xEl) * this.LERP
this.yEl += (this.targetYEl - this.yEl) * this.LERP
this.xInner += (this.targetXInner - this.xInner) * this.LERP
this.yInner += (this.targetYInner - this.yInner) * this.LERP
this.el.style.transform = `translate(${this.xEl}px, ${this.yEl}px)`
this.inner.style.transform = `translate(${this.xInner}px, ${this.yInner}px)`
this.rafId = requestAnimationFrame(this.tick)
}
destroy() {
if (this.rafId) cancelAnimationFrame(this.rafId)
window.removeEventListener('mousemove', this.onMouseMove)
window.removeEventListener('resize', this.recalcBounds)
this.el.style.transform = ''
this.inner.style.transform = ''
}
} מבנה HTML ו-CSS
שתי שכבות: מכלול חיצוני ו-span פנימי עם טקסט. ההזזות שונות.
<button class="magnetic-btn" data-magnetic>
<span class="magnetic-btn__inner" data-magnetic-inner> Связаться </span>
</button>
<style>
.magnetic-btn {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 16px 40px;
border-radius: 100px;
background: #1a1a1a;
color: #fff;
border: none;
cursor: none;
will-change: transform;
transition: background 0.3s ease;
}
.magnetic-btn__inner {
display: block;
will-change: transform;
pointer-events: none;
}
</style> יישום בטכנולוגיות שונות
JavaScript טהור (LERP)
המחלקה interface MagneticConfig { strength: number // сила притяжения, 0.3–0.6 innerStrength: number // сила для внутреннего контента, 0.6–1.2 radius: number // множитель зоны притяжения от размера кнопки } class MagneticButton { private el: HTMLElement private inner: HTMLElement private bounds: DOMRect private config: MagneticConfig private rafId: number | null = null // Текущие смещения (анимированные) private xEl = 0 private yEl = 0 private xInner = 0 private yInner = 0 // Целевые смещения private targetXEl = 0 private targetYEl = 0 private targetXInner = 0 private targetYInner = 0 private readonly LERP = 0.15 constructor(el: HTMLElement, config: Partial<MagneticConfig> = {}) { this.el = el this.inner = el.querySelector('[data-magnetic-inner]') || el this.config = { strength: 0.4, innerStrength: 0.8, radius: 1.6, ...config, } this.bounds = el.getBoundingClientRect() this.init() } private init() { window.addEventListener('mousemove', this.onMouseMove) window.addEventListener('resize', this.recalcBounds) this.tick() } private recalcBounds = () => { this.bounds = this.el.getBoundingClientRect() } private onMouseMove = (e: MouseEvent) => { const { left, top, width, height } = this.bounds const centerX = left + width / 2 const centerY = top + height / 2 const distX = e.clientX - centerX const distY = e.clientY - centerY const distance = Math.sqrt(distX ** 2 + distY ** 2) const threshold = (Math.max(width, height) / 2) * this.config.radius if (distance < threshold) { const force = (threshold - distance) / threshold this.targetXEl = distX * this.config.strength * force this.targetYEl = distY * this.config.strength * force this.targetXInner = distX * this.config.innerStrength * force this.targetYInner = distY * this.config.innerStrength * force } else { this.targetXEl = 0 this.targetYEl = 0 this.targetXInner = 0 this.targetYInner = 0 } } private tick = () => { this.xEl += (this.targetXEl - this.xEl) * this.LERP this.yEl += (this.targetYEl - this.yEl) * this.LERP this.xInner += (this.targetXInner - this.xInner) * this.LERP this.yInner += (this.targetYInner - this.yInner) * this.LERP this.el.style.transform = `translate(${this.xEl}px, ${this.yEl}px)` this.inner.style.transform = `translate(${this.xInner}px, ${this.yInner}px)` this.rafId = requestAnimationFrame(this.tick) } destroy() { if (this.rafId) cancelAnimationFrame(this.rafId) window.removeEventListener('mousemove', this.onMouseMove) window.removeEventListener('resize', this.recalcBounds) this.el.style.transform = '' this.inner.style.transform = '' } } למעלה היא פתרון מוכן ללא תלות בספריות. משתמשת ב-<button class="magnetic-btn" data-magnetic> <span class="magnetic-btn__inner" data-magnetic-inner> Связаться </span> </button> <style> .magnetic-btn { position: relative; display: inline-flex; align-items: center; justify-content: center; padding: 16px 40px; border-radius: 100px; background: #1a1a1a; color: #fff; border: none; cursor: none; will-change: transform; transition: background 0.3s ease; } .magnetic-btn__inner { display: block; will-change: transform; pointer-events: none; } </style> ובאינטרפולציה לינארית (LERP). יתרונות: שליטה מלאה. חסרונות: עלולים להופיע ארטיפקטים בתנועות מהירות.
GSAP quickTo
אם GSAP כבר קיים בפרויקט, החליפו את אנימציית ה-lerp ב-MagneticButton — חלקה יותר, ללא ארטיפקטים. GSAP quickTo מהירה ב-30% במעבד מאשר LERP ידני.
import gsap from 'gsap'
class MagneticButtonGSAP {
private el: HTMLElement
private xSetter: (value: number) => void
private ySetter: (value: number) => void
constructor(el: HTMLElement) {
this.el = el
this.xSetter = gsap.quickTo(el, 'x', { duration: 0.6, ease: 'power3' })
this.ySetter = gsap.quickTo(el, 'y', { duration: 0.6, ease: 'power3' })
el.addEventListener('mousemove', this.onMove)
el.addEventListener('mouseleave', this.onLeave)
}
private onMove = (e: MouseEvent) => {
const rect = this.el.getBoundingClientRect()
const x = e.clientX - rect.left - rect.width / 2
const y = e.clientY - rect.top - rect.height / 2
this.xSetter(x * 0.35)
this.ySetter(y * 0.35)
}
private onLeave = () => {
this.xSetter(0)
this.ySetter(0)
}
} Framer Motion (React)
בפרויקטים של React, אנו משתמשים ב-requestAnimationFrame וב-gsap.quickTo מ-framer-motion. רכיב העטיפה import gsap from 'gsap' class MagneticButtonGSAP { private el: HTMLElement private xSetter: (value: number) => void private ySetter: (value: number) => void constructor(el: HTMLElement) { this.el = el this.xSetter = gsap.quickTo(el, 'x', { duration: 0.6, ease: 'power3' }) this.ySetter = gsap.quickTo(el, 'y', { duration: 0.6, ease: 'power3' }) el.addEventListener('mousemove', this.onMove) el.addEventListener('mouseleave', this.onLeave) } private onMove = (e: MouseEvent) => { const rect = this.el.getBoundingClientRect() const x = e.clientX - rect.left - rect.width / 2 const y = e.clientY - rect.top - rect.height / 2 this.xSetter(x * 0.35) this.ySetter(y * 0.35) } private onLeave = () => { this.xSetter(0) this.ySetter(0) } } משתלב בקלות בכל כפתור.
import { useRef, useCallback } from 'react'
import { motion, useMotionValue, useSpring } from 'framer-motion'
interface MagneticProps {
children: React.ReactNode
strength?: number
}
export function Magnetic({ children, strength = 0.4 }: MagneticProps) {
const ref = useRef<HTMLDivElement>(null)
const xRaw = useMotionValue(0)
const yRaw = useMotionValue(0)
const x = useSpring(xRaw, { stiffness: 200, damping: 15 })
const y = useSpring(yRaw, { stiffness: 200, damping: 15 })
const onMove = useCallback((e: React.MouseEvent) => {
if (!ref.current) return
const rect = ref.current.getBoundingClientRect()
const dx = e.clientX - rect.left - rect.width / 2
const dy = e.clientY - rect.top - rect.height / 2
xRaw.set(dx * strength)
yRaw.set(dy * strength)
}, [strength])
const onLeave = useCallback(() => {
xRaw.set(0)
yRaw.set(0)
}, [])
return (
<motion.div
ref={ref}
style={{ x, y, display: 'inline-block' }}
onMouseMove={onMove}
onMouseLeave={onLeave}
>
{children}
</motion.div>
)
} השוואת גישות
| גישה | תלות בספריות | ביצועים | קלות אינטגרציה |
|---|---|---|---|
| JavaScript טהור (LERP) | אין | גבוהים (requestAnimationFrame) | בינונית (דורש קוד) |
| GSAP quickTo | GSAP (9KB) | גבוהים (setter מותאם) | גבוהה (קוד מינימלי) |
| Framer Motion | framer-motion (30KB) | בינוניים (useSpring) | גבוהה (ידידותי ל-React) |
טעויות נפוצות והפתרונות שלהן
| טעות | סיבה | פתרון |
|---|---|---|
| ארטיפקטים בתנועות מהירות | קצב פריימים נמוך או צעד LERP גדול | השתמשו ב-GSAP quickTo או הגדילו את LERP ל-0.2 |
| ההזזה נשארת אחרי שהסמן עוזב | targetX/Y לא מאופסים ב-mouseleave | הוסיפו handler של mouseleave עם איפוס |
| ריצוד במכשירים ניידים | האפקט מופעל במכשירי מגע | השביתו באמצעות matchMedia('(hover: hover)') |
יישום שלב אחר שלב: מניתוח ועד פריסה
- ניתוח ממשק ובחירת גישה. למדו את הטכנולוגיה הנוכחית, גדלי הכפתורים, ההקשר. קבעו אם יש צורך בסמן מותאם אישית או שהסמן הרגיל מספיק.
- פיתוח רכיב. כתבו את היישום בטכנולוגיה שנבחרה (JS/GSAP/framer-motion). הגדירו פרמטרים: עוצמה, רדיוס, innerStrength.
- אינטגרציה לפרויקט. שלבו את הרכיב בכל כפתורי ה-CTA. עבור SPA/Next.js, הוסיפו לוגיקת destroy/reinit בניווט.
- בדיקות. בדקו בדסקטופ (Chrome, Firefox, Safari) ובמכשירים ניידים. מדדו LCP, CLS, INP לפני ואחרי.
- אופטימיזציה. הפעילו האצת GPU, השביתו במכשירי מגע, הגדירו will-change.
- פריסה ותיעוד. ספקו את הרכיב עם הערות, קונפיגורציה והוראות.
מה כלול בעבודה?
- ניתוח ובחירת טכנולוגיה. קביעת הגישה האופטימלית לטכנולוגיה שלכם (React, Vue, Angular, JavaScript טהור).
- פיתוח רכיב. יצירת רכיב ניתן להגדרה עם פרמטרים
useMotionValue,useSpring,Magnetic. - אינטגרציה. שילוב בכל כפתורי ה-CTA באתר, הגדרת השבתה אוטומטית במכשירי מגע.
- בדיקות ביצועים. מדידת LCP, CLS, INP לפני ואחרי, הבטחה ללא ירידה בביצועים.
- תיעוד. מתן הערות קוד, תיאור קונפיגורציה והוראות להתאמה אישית נוספת.
- תמיכה. תיקונים חינם תוך חודש לאחר מסירת הפרויקט.
לוח זמנים ועלות
לוחות זמנים משוערים: יישום פשוט (JavaScript טהור) — מ-4 עד 6 שעות; עם GSAP או framer-motion ורכיב React — 1–2 ימים. העלות מחושבת באופן אישי לאחר הערכת הפרויקט.
הזמינו יישום של האפקט המגנטי לאתר שלכם. קבלו ייעוץ.
למה לסמוך עלינו?
יש לנו ניסיון של 5+ שנים בפיתוח פרונטאנד, מעל 20 פרויקטים עם אנימציה. אנו מבטיחים:
- התאמה לטכנולוגיה שלכם (React, Vue, Angular, JavaScript טהור).
- אופטימיזציה ל-Core Web Vitals (LCP < 2.5s, CLS = 0).
- תמיכה במכשירי מגע (השבתה אוטומטית ב-hover: none).
- תיעוד והערות קוד.
- אחריות על קוד (3 חודשים) ותמיכה חינם לחודש לאחר המסירה.
השאירו בקשה ליישום האפקט המגנטי לאתר שלכם. צרו קשר לייעוץ — נעריך את הפרויקט ונציע את הפתרון האופטימלי.







