Что такое 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 месяца) и бесплатная поддержка в течение месяца после сдачи.
Оставьте заявку на реализацию магнитного эффекта для вашего сайта. Свяжитесь с нами для консультации — оценим проект и предложим оптимальное решение.







