Build a custom cursor follower

Code
20 February 2025
Demo
Cursor Follower Feature Image

This cursor follower displays a circular “Drag Me!” hint over a Swiper slider. It follows the pointer while it is over the slider and hides when it leaves.

The example targets an existing slider at #hero-slider .swiper-wrapper. The follower is a separate element layered over the page.

HTML

Add a container for the circle and an inner element for its label. Separate elements let you animate the circle and text independently.

<div class="cursor">
<div class="cursor_content">
<span>Drag Me!</span>
</div>
</div>

CSS

Give the follower a fixed position and start it hidden. pointer-events: none lets pointer interactions pass through to the slider.

.cursor {
width: 80px;
height: 80px;
border-radius: 50%;
position: fixed;
transform-origin: center;
pointer-events: none !important;
z-index: 100;
transition:
transform 0.4s cubic-bezier(0.195, 1, 0.225, 1),
opacity 1.4s cubic-bezier(0.195, 1, 0.225, 1);
transform: scale(0);
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
color: #fff;
background-color: #000;
}

.cursor_content {
transition:
transform 0.5s cubic-bezier(0.19, 1, 0.22, 1),
opacity 1.4s linear;
opacity: 0;
transition-delay: 0.15s;
}

The circle uses transform for movement and scaling. Its inner label has a short transition delay so the text appears after the circle starts opening.

JavaScript

Track the pointer position and show the follower while it is over the slider.

function initCursorFollower(lastMouseX = null, lastMouseY = null) {
const cursor = document.querySelector('.cursor')
const cursorContent = document.querySelector('.cursor_content')
const hoverSection = document.querySelector('#hero-slider .swiper-wrapper')

const cursorWidth = 80
const cursorHeight = 80
let isHovering = false

const updateCursorPosition = (x, y) => {
const translateX = x - cursorWidth / 2
const translateY = y - cursorHeight / 2
cursor.style.transform = `translate(${translateX}px, ${translateY}px) scale(${isHovering ? 1 : 0})`
}

const updateHoverState = () => {
if (lastMouseX === null || lastMouseY === null) return

const bounds = hoverSection.getBoundingClientRect()
isHovering = lastMouseX >= bounds.left && lastMouseX < bounds.right &&
lastMouseY >= bounds.top && lastMouseY < bounds.bottom

updateCursorPosition(lastMouseX, lastMouseY)
cursor.style.opacity = isHovering ? '1' : '0'
cursorContent.style.opacity = isHovering ? '1' : '0'
cursorContent.style.transform = `scale(${isHovering ? 1 : 0})`
}

const updatePointerPosition = (event) => {
lastMouseX = event.clientX
lastMouseY = event.clientY
updateHoverState()
}

document.addEventListener('pointermove', updatePointerPosition)
document.addEventListener('scroll', updateHoverState, { capture: true, passive: true })
hoverSection.addEventListener('mouseenter', updatePointerPosition)
hoverSection.addEventListener('mouseleave', updatePointerPosition)

updateHoverState()
}

How it works

  • pointermove stores the pointer coordinates and calls updateHoverState.

  • updateCursorPosition subtracts half the follower width and height to center it on those coordinates.

  • mouseenter and mouseleave refresh the pointer coordinates and visibility using the same centering calculation.

  • scroll rechecks the slider bounds using the last pointer position. Scroll events do not provide pointer coordinates.

  • The inner label has its own scale and opacity transitions.

Adjust the effect

Change the label, colors, or transition timing to suit the section. If you change the 80px circle size, update cursorWidth and cursorHeight in JavaScript too; the centering calculation depends on them.

Want to see an example? Demo