<button class="btn btn--1" data-dist="200" data-smooth="0.05"></button>
<button class="btn btn--2" data-dist="300" data-smooth="0.2"></button>
<button class="btn btn--3" data-dist="150" data-smooth="0.1"></button>
<button class="btn btn--4" data-dist="100" data-smooth="0.15"></button>
* {
  box-sizing: border-box;
}

.btn {
  display: flex;
  justify-content: center;
  align-items: center;
  position: relative;
  padding: 2em;
  border-radius: 50%;

  &::before {
    content: "";
    position: absolute;
  }
}

.btn--1 {
  margin-top: 10vh;
  
  &::before {
    content: "<1>";
  }
}

.btn--2 {
  margin-top: 25vh;
  margin-left: 15vw;
  
  &::before {
    content: "<2>";
  }
}

.btn--3 {
  margin-top: 75vh;
  margin-left: 50vw;
  
  &::before {
    content: "<3>";
  }
}

.btn--4 {
  margin-top: 120vh;
  margin-left: 75vw;
  
  &::before {
    content: "<4>";
  }
}
View Compiled
const lerp = (v0, v1, t) => v0 * (1 - t) + v1 * t;

class MagneticEl {
  static {
    this.addGlobalEvents();
    this.beginRafLoop();
  }

  static instances = [];
  static mouse = { x: 0, y: 0 };
  
  constructor(selector, scroller = window) {
    this.el = typeof selector === 'string' ? document.querySelector(selector) : selector;
    this.scroller = scroller;
    this.rect = this.calcPageRect();
    this.position = { ...this.rect };
    
    this.smooth = parseFloat(this.el.dataset.smooth) || 0.05;
    this.dist = parseFloat(this.el.dataset.dist) || 200;
    
    this.constructor.instances.push(this);
  }
  
  calcPageRect() {
    const transform = this.el.style.transform;
    this.el.style.transform = '';
    const rect = this.el.getBoundingClientRect();
    this.el.style.transform = transform;
    
    return {
      x: rect.x + (this.scroller === window ? window.scrollX : this.scroller.scrollLeft),
      y: rect.y + (this.scroller === window ? window.scrollY : this.scroller.scrollTop),
      w: rect.width,
      h: rect.height,
    };
  }
  
  getScreenRect() {
    return {
      x: this.rect.x - (this.scroller === window ? window.scrollX : this.scroller.scrollLeft),
      y: this.rect.y - (this.scroller === window ? window.scrollY : this.scroller.scrollTop),
      w: this.rect.w,
      h: this.rect.h,
    };
  }
  
  updateTransforms() {
    const pos = this.getScreenRect();
    const { x: mx, y: my } = this.constructor.mouse;
    
    const dist = Math.hypot(pos.x - mx, pos.y - my);
    
    if (dist < this.dist) {
      this.position.x = lerp(this.position.x, mx - pos.w / 2, this.smooth);
      this.position.y = lerp(this.position.y, my - pos.h / 2, this.smooth);
    }
    else {
      this.position.x = lerp(this.position.x, pos.x, this.smooth);
      this.position.y = lerp(this.position.y, pos.y, this.smooth);
    }
    
    const tx = this.position.x - pos.x;
    const ty = this.position.y - pos.y;
 
    this.el.style.transform = `translate(${tx}px, ${ty}px)`;
  }
  
  static addGlobalEvents() {
    window.addEventListener('resize', (e) => {
      this.instances.forEach(item => {
        item.rect = item.calcPageRect();
        item.position = { ...item.rect };
      });
    });
    
    window.addEventListener('mousemove', (e) => {
      this.mouse.x = e.clientX;
      this.mouse.y = e.clientY;
    });
  }
  
  static beginRafLoop() {
    const loop = (now) => {
      this.instances.forEach(item => item.updateTransforms());
      requestAnimationFrame(loop);
    }
    
    requestAnimationFrame(loop);
  }
}

document.querySelectorAll('.btn')
  .forEach(el => new MagneticEl(el));

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.