<body>
  <div class="displayed-cursor">
    <div class="displayed-cursor-circle"></div>
  </div>
  <div class="container">
    <div class="target-element">
      <div class="target-element-circle"></div>
    </div>
    <div class="target-element">
      <div class="target-element-circle"></div>
    </div>
    <div class="target-element">
      <div class="target-element-circle"></div>
    </div>
    <div class="target-element">
      <div class="target-element-circle"></div>
    </div>
    <div class="target-element">
      <div class="target-element-circle"></div>
    </div>
    <div class="target-element">
      <div class="target-element-circle"></div>
    </div>
    <div class="target-element">
      <div class="target-element-circle"></div>
    </div>
    <div class="target-element">
      <div class="target-element-circle"></div>
    </div>
    <div class="target-element">
      <div class="target-element-circle"></div>
    </div>
  </div>
</body>
*,
*::before,
*::after {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  background-color: #000;
  min-height: 100vh;
  overflow: hidden;
  cursor: none;
}

.displayed-cursor {
  position: absolute;
  top: -10px;
  left: -10px;
  z-index: 10000;
  mix-blend-mode: difference;
  pointer-events: none;
  transition: transform 0.03s;
}

.displayed-cursor-circle {
  position: absolute;
  top: 0;
  left: 0;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background-color: #fff;
  transition: transform 0.3s;
}

.container {
  color: floralwhite;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  width: 210px;
  height: 210px;
}

.target-element {
  display: flex;
  flex-flow: row nowrap;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100%;
}

.target-element-circle {
  background-color: #eee;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  transition: transform 0.2s;
  pointer-events: none;
}

.on-target-element {
  transform: scale3d(2.5, 2.5, 1);
}
const $displayedCursor = document.querySelector(".displayed-cursor");
const $targetElements = document.querySelectorAll(".target-element");
let $targetElementCircle;

let actualCursorX = 0;
let actualCursorY = 0;

let displayedCursorX = 0;
let displayedCursorY = 0;

let targetElementX = 0;
let targetElementY = 0;

let targetElementWidth = 0;
let targetElementHeight = 0;

let targetElementActualCursorX = 0;
let targetElementActualCursorY = 0;

let targetElementCircleX = 0;
let targetElementCircleY = 0;

let displayedCursorIsSticked = false;

const handleActualCursorPositionSettingInDocument = (event) => {
  actualCursorX = event.pageX;
  actualCursorY = event.pageY;
};

const handleActualCursorPositionSettingInTargetElement = (event) => {
  targetElementActualCursorX = event.offsetX;
  targetElementActualCursorY = event.offsetY;
};

const handleDisplayedCursorSticksToTargetElement = (event) => {
  targetElementX = event.target.offsetLeft;
  targetElementY = event.target.offsetTop;
  targetElementWidth = event.target.offsetWidth;
  targetElementHeight = event.target.offsetHeight;

  $targetElementCircle = event.target.firstElementChild;

  displayedCursorIsSticked = true;
  triggerDisplayedCursorStickAnimation();
};

const handleDisplayedCursorDeviateFromTargetElement = () => {
  displayedCursorIsSticked = false;

  triggerDisplayedCursorAnimation();
};

const triggerDisplayedCursorAnimation = (timestamp) => {
  if (!displayedCursorIsSticked) {
    requestAnimationFrame(triggerDisplayedCursorAnimation);
  }

  // 표시 커서가 실제 커서를 따라다니는 효과.
  displayedCursorX -= (displayedCursorX - actualCursorX) * 0.8;
  displayedCursorY -= (displayedCursorY - actualCursorY) * 0.8;

  $displayedCursor.style.transform = `translate3D(${displayedCursorX}px, ${displayedCursorY}px, 0)`;

  // 표시 커서가 작아지는 효과.
  $displayedCursor.firstElementChild.classList.remove("on-target-element");

  // 목표 요소가 제자리로 돌아가는 효과.
  targetElementCircleX = 0;
  targetElementCircleY = 0;
  if ($targetElementCircle) {
    $targetElementCircle.style.transform = `translate3D(${targetElementCircleX}px, ${targetElementCircleY}px, 0)`;
  }
};

const triggerDisplayedCursorStickAnimation = (timestamp) => {
  if (displayedCursorIsSticked) {
    requestAnimationFrame(triggerDisplayedCursorStickAnimation);
  }

  // 표시 커서가 목표 요소 중앙으로 이동하는 동시에, 실제 커서 위치에 반응해 끌려가는 효과.
  displayedCursorX -=
    (displayedCursorX -
      targetElementX -
      targetElementWidth / 2 -
      (targetElementActualCursorX - targetElementWidth / 2) * 0.2) *
    0.2;
  displayedCursorY -=
    (displayedCursorY -
      targetElementY -
      targetElementHeight / 2 -
      (targetElementActualCursorY - targetElementHeight / 2) * 0.2) *
    0.2;
  $displayedCursor.style.transform = `translate3D(${displayedCursorX}px, ${displayedCursorY}px, 0)`;

  // 표시 커서 크기가 커지는 효과.
  $displayedCursor.firstElementChild.classList.add("on-target-element");

  // 목표 요소가 실제 커서 위치에 반응해 끌려가는 효과.
  targetElementCircleX =
    (targetElementActualCursorX - targetElementWidth / 2) * 0.2;
  targetElementCircleY =
    (targetElementActualCursorY - targetElementHeight / 2) * 0.2;
  $targetElementCircle.style.transform = `translate3D(${targetElementCircleX}px, ${targetElementCircleY}px, 0)`;
};

document.addEventListener(
  "mousemove",
  handleActualCursorPositionSettingInDocument
);

$targetElements.forEach(($targetElement) => {
  $targetElement.addEventListener(
    "mousemove",
    handleActualCursorPositionSettingInTargetElement
  );
});

$targetElements.forEach(($targetElement) => {
  $targetElement.addEventListener(
    "mouseenter",
    handleDisplayedCursorSticksToTargetElement
  );
});

$targetElements.forEach(($targetElement) => {
  $targetElement.addEventListener(
    "mouseleave",
    handleDisplayedCursorDeviateFromTargetElement
  );
});

triggerDisplayedCursorAnimation();
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.