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
pointermovestores the pointer coordinates and callsupdateHoverState.updateCursorPositionsubtracts half the follower width and height to center it on those coordinates.mouseenterandmouseleaverefresh the pointer coordinates and visibility using the same centering calculation.scrollrechecks 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.