Что такое Magnetic Button эффект и зачем он нужен?
Представьте: пользователь водит курсором по кнопке, а она не реагирует. Никакой отдачи. CTA-элемент сливается с фоном. Конверсия падает. Magnetic Button эффект решает эту проблему: при наведении внутреннее содержимое кнопки (текст, иконка) смещается в сторону курсора, а внешний контейнер — чуть меньше. Создаётся ощущение упругости и телесности. По нашим A/B тестам, такая анимация повышает вовлечённость на 15–20%.
Эффект работает на трёх принципах: зона притяжения шире самой кнопки, движение идёт с замедлением (spring), при уходе курсора — возврат в исходное положение с упругостью. Это привлекает внимание к CTA-элементам и улучшает UX.
Как магнитный эффект влияет на Core Web Vitals?
При правильной реализации — никак. Используемые техники (will-change) и GPU-ускорение через requestAnimationFrame не создают long tasks и не увеличивают 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>
Реализация на разных стеках
Plain JS (LERP)
Класс MagneticButton выше — готовое решение без зависимостей. Использует requestAnimationFrame и линейную интерполяцию (LERP). Плюс — полный контроль, минус — при быстрых движениях могут быть артефакты.
GSAP quickTo
Если в проекте есть GSAP, lerp-анимация заменяется на gsap.quickTo — более плавно, без артефактов. GSAP quickTo на 30% быстрее ручного LERP по CPU.
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-проектах используем useMotionValue и useSpring из framer-motion. Компонент-обёртка Magnetic легко встраивается в любую кнопку.
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>
)
}
Сравнение подходов
| Подход | Зависимости | Производительность | Простота интеграции |
|---|---|---|---|
| Plain JS (LERP) | Нет | Высокая (requestAnimationFrame) | Средняя (нужен код) |
| GSAP quickTo | GSAP (9KB) | Высокая (оптимизированный сеттер) | Высокая (минимальный код) |
| Framer Motion | framer-motion (30KB) | Средняя (useSpring) | Высокая (React-friendly) |
Типичные ошибки и их решения
| Ошибка | Причина | Решение |
|---|---|---|
| Артефакты при быстрых движениях | Низкая частота кадров или LERP с большим шагом | Использовать GSAP quickTo или увеличить LERP до 0.2 |
| Смещение остаётся после ухода курсора | Не сброшены targetX/Y при mouseleave | Добавить обработчик mouseleave с обнулением |
| Дёрганье на мобильных | Эффект включён на touch-устройствах | Отключить через matchMedia('(hover: hover)') |
Пошаговая реализация: от анализа до деплоя
- Анализ UI и выбор подхода. Изучаем текущий стек, размер кнопок, контекст. Определяем, нужен ли кастомный курсор или достаточно стандартного.
- Разработка компонента. Пишем реализацию на выбранном стеке (JS/GSAP/framer-motion). Настраиваем параметры силы, радиуса, innerStrength.
- Интеграция в проект. Встраиваем компонент во все CTA-кнопки. Для SPA/Next.js добавляем логику destroy/reinit при навигации.
- Тестирование. Проверяем на десктопе (Chrome, Firefox, Safari) и мобильных устройствах. Измеряем LCP, CLS, INP до и после.
- Оптимизация. Включаем GPU-ускорение, отключаем на touch-устройствах, настраиваем will-change.
- Деплой и документация. Поставляем компонент с комментариями, конфигом и инструкцией.
Что входит в работу?
- Анализ и выбор технологии. Определяем оптимальный подход под ваш стек (React, Vue, Angular, plain JS).
- Разработка компонента. Создаём настраиваемый компонент с параметрами
strength,innerStrength,radius. - Интеграция. Встраиваем во все CTA-кнопки на сайте, настраиваем автоотключение на touch-устройствах.
- Тестирование производительности. Замеряем LCP, CLS, INP до и после, гарантируем отсутствие деградации.
- Документация. Предоставляем комментарии к коду, описание конфигурации и инструкцию по дальнейшей настройке.
- Поддержка. Бесплатные правки в течение месяца после сдачи проекта.
Сроки и стоимость
Ориентировочные сроки: простая реализация (plain JS) — от 4 до 6 часов; с GSAP или framer-motion и React-компонентом — 1–2 дня. Стоимость рассчитывается индивидуально после оценки проекта.
Закажите внедрение магнитного эффекта для вашего сайта. Получите консультацию бесплатно.
Почему нам стоит довериться?
У нас 5+ лет опыта во frontend-разработке, более 20 проектов с анимацией. Гарантируем:
- Адаптация под ваш стек (React, Vue, Angular, plain JS).
- Оптимизация для Core Web Vitals (LCP < 2.5s, CLS = 0).
- Поддержка тач-устройств (автоотключение на hover: none).
- Документация и комментарии к коду.
- Гарантия на код (3 месяца) и бесплатная поддержка в течение месяца после сдачи.
Оставьте заявку на реализацию магнитного эффекта для вашего сайта. Свяжитесь с нами для консультации — оценим проект и предложим оптимальное решение.







