Реалізація 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 — при зміні макету через CSS (наприклад, sticky header) getBoundingClientRect застаріває. Потрібен ResizeObserver + пересчет bounds при scroll.

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

Продуктивність на мобілці — на touch-пристроях цей ефект не потрібен (немає hover). Додаємо перевірку matchMedia('(hover: hover)') та пропускаємо ініціалізацію.

Терміни

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