Реализация Magnetic Button эффекта на сайте

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация Magnetic Button эффекта на сайте
Средняя
от 1 рабочего дня до 3 рабочих дней
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Реализация Magnetic Button эффекта на сайте

Magnetic Button — кнопка, которая притягивается к курсору при приближении. Не сам элемент сдвигается в сторону мыши — внутреннее содержимое (текст, иконка) смещается сильнее, чем внешний контейнер. Это создаёт эффект упругости и телесности.

Эффект работает на трёх принципах: зона притяжения шире самой кнопки, движение идёт с замедлением (spring), при уходе курсора — возврат в исходное положение с упругостью.

Математика притяжения

Для каждого кадра считается расстояние от центра кнопки до курсора. Если курсор в зоне притяжения — вычисляется смещение, пропорциональное расстоянию и нормализованное до максимального значения.

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  // 0..1

      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 {
      // Вне зоны — возврат в 0
      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-структура

Два слоя: внешний контейнер и внутренний span с текстом. Смещения — разные.

<button class="magnetic-btn" data-magnetic>
  <span class="magnetic-btn__inner" data-magnetic-inner>
    Связаться
  </span>
</button>
.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;
}

Реализация через GSAP

Если в проекте есть GSAP, lerp-анимация заменяется на gsap.quickTo — более плавно, без артефактов при быстром движении.

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

    // quickTo создаёт оптимизированный setter с spring
    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)
  }
}

React-компонент

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>
  )
}

Особенности и ограничения

ResizeObserver вместо window resize — при изменении layout через CSS (например, sticky header) getBoundingClientRect устаревает. Нужен ResizeObserver + пересчёт bounds при scroll.

Многократная инициализация — если страница подгружает контент через AJAX или SPA-навигацию, нужна логика destroy + reinit. Паттерн: хранить Map (element -> instance) и проверять перед созданием.

Performance на мобилке — на тач-устройствах эффект не нужен (нет hover). Добавляем проверку matchMedia('(hover: hover)') и пропускаем инициализацию.

Сроки

Готовая реализация с plain JS/TS, без зависимостей — 4–6 часов. С GSAP или framer-motion, React-компонентом и поддержкой custom cursor — 1–2 дня.