One of the most important aspects when designing a digital product, alongside accessibility, is the user experience. It basically defines how a person interacts with what we have built.
And there is one thing that often makes a difference: the interface reacting. Feeling that what you do has an immediate response makes everything feel more alive and polished. It is true that adding these kinds of interactions does not always make sense, but when they are used well in the right context and at the right moment, they greatly improve the final result.
One of the simplest and most eye-catching effects is cursor tracking. And although it may seem complex at first glance, it is actually super easy to build.
What Exactly Is Cursor Tracking?
Cursor tracking consists of detecting the cursor position and using it to update the position of an element on the screen.
From a technical perspective, it can be summarized in three steps:
- Listen to mouse movement (
mousemove) - Retrieve its coordinates (
clientX,clientY) - Update the DOM using those values to create the interaction.
Basic Cursor Tracking Implementation
Now let’s see how we can implement a simple cursor tracking effect. In this case, we are going to build a small trail that smoothly follows the cursor using GSAP, since it allows us to manage animations much more easily and efficiently than with pure CSS.
Here is the full example on CodePen:
The HTML: Preparing the Trail Elements
For this effect, we need several elements that will form the trail. Each of these divs will act as a “particle” that follows the cursor position with a slight delay relative to the previous one.
<div class="trail"></div>
<div class="trail"></div>
<div class="trail"></div>
<div class="trail"></div>
<div class="trail"></div>
<div class="trail"></div>
The CSS: Positioning and Styling
Next, we give them a basic style so they behave like small dots. The most important parts here are position: fixed, so they move relative to the viewport, and pointer-events: none, so the circles do not intercept clicks intended for other buttons or links on the page.
body {
height: 200vh;
background: #111111;
overflow: hidden;
}
.trail {
width: 20px;
height: 20px;
background: #d9ed92;
border-radius: 50%;
position: fixed;
top: 0;
left: 0;
pointer-events: none;
}
Then we are going to center the particles when the page loads. If we do not do this, the effect would suddenly start from the top-left corner of the screen (0,0) the first time we move the mouse. With gsap.set, we position them directly in the center:
// Center the particles on load to avoid the default (0,0) position
const centerX = window.innerWidth / 2;
const centerY = window.innerHeight / 2;
trails.forEach((el) => {
gsap.set(el, {
x: centerX,
y: centerY
});
});
It is important to mention that this is not strictly necessary. There are many ways to approach it. For example, you could hide the particles with zero opacity and make them appear only when the user moves the mouse. But in our example, since there is nothing else on the screen, it makes more sense for the elements to already be positioned and ready to react from the center.
Finally, we add an event listener that will detect mouse movement. On every movement, we trigger a GSAP animation for each particle:
// Cursor tracking with staggered delay to create a smooth trailing effect
window.addEventListener("mousemove", (e) => {
trails.forEach((el, index) => {
gsap.to(el, {
x: e.clientX,
y: e.clientY,
duration: 0.2 + index * 0.05, // We use the index so each particle is slightly slower than the previous one
ease: "power2.out"
});
});
});
By using the array index to calculate the duration, we make the first particle almost instantaneous while the following ones move with a slight delay. The result is that organic movement feeling that makes the effect much more visually appealing.
Important: for this example to work correctly, we need to include GSAP in our project. We can do so by adding the following script:
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15/dist/gsap.min.js"></script>
Advanced Cursor Tracking: Creating Eyes That Follow You
We have already seen how to make something follow the cursor, but cursor tracking does not always have to involve full movement across the screen. We can use the same coordinate logic to create subtler and more playful interactions, such as eyes that follow the mouse movement.
In this case, instead of moving the elements themselves, we are going to calculate the angle and distance so that the pupils always stay inside the eye.
You can see the result on CodePen:
The HTML: Eye Structure
For this to work, we need a container for each eye and an inner element for the pupil.
<div class="eyes-container">
<div class="eye">
<div class="pupil"></div>
</div>
<div class="eye">
<div class="pupil"></div>
</div>
</div>
The CSS: Constraining the Movement
The trick here is making sure the .eye container has overflow: hidden. This guarantees that, even if our calculations go beyond the limits, the pupil will never leave the white area of the eye (or, if it does exceed its container, we simply will not see it).
body {
margin: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100dvh;
background-color: #f5f5f5;
overflow: hidden;
}
.eyes-container {
display: flex;
gap: 20px;
}
.eye {
width: 100px;
height: 100px;
background-color: white;
border: 4px solid #1a1a1a;
border-radius: 50%;
position: relative;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
.pupil {
width: 40px;
height: 40px;
background-color: #1a1a1a;
border-radius: 50%;
position: absolute;
}
The JavaScript: A Bit of Trigonometry and GSAP
This is where things get interesting. To make the eyes look toward the mouse, we need to know the angle between the center of the eye and the cursor.
First, we select the pupils, which are the elements we are going to move:
const pupils = document.querySelectorAll(".pupil");
Now we calculate the movement. Math.atan2 gives us the angle, while Math.cos / Math.sin tells us how much to move the pupil on the X and Y axes so it points in that direction:
window.addEventListener("mousemove", (e) => {
pupils.forEach((pupil) => {
// Get the parent eye element and its center position
const eye = pupil.parentElement;
const rect = eye.getBoundingClientRect();
const eyeCenterX = rect.left + rect.width / 2;
const eyeCenterY = rect.top + rect.height / 2;
// Calculate the angle between the mouse and the eye center
const angle = Math.atan2(
e.clientY - eyeCenterY,
e.clientX - eyeCenterX
);
// Define the maximum movement radius for the pupil
const maxDistance = 25;
// Measure mouse distance from the eye center and clamp it
const mouseDistance = Math.hypot(
e.clientX - eyeCenterX,
e.clientY - eyeCenterY
);
const distance = Math.min(mouseDistance / 10, maxDistance);
// Calculate pupil offset based on angle and clamped distance
const moveX = Math.cos(angle) * distance;
const moveY = Math.sin(angle) * distance;
// Animate the pupil for a smooth
gsap.to(pupil, {
x: moveX,
y: moveY,
duration: 0.4,
ease: "power2.out",
overwrite: "auto"
});
});
});
Important: just like in the previous example, for this to work correctly we need to include GSAP in our project. We can do so by adding the following script:
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15/dist/gsap.min.js"></script>
As you have seen, once you understand how to capture mouse coordinates, the possibilities are almost endless (and I say “almost” because there will always be something slightly impossible). But with this foundation, we can go from a simple trail effect to a much more advanced animation with only a few changes in the JavaScript logic.
And the best thing about using GSAP for these effects is that it takes away the heavy lifting of managing frames and smoothing, allowing us to focus on what really matters: making the interaction feel natural and adding value to the user experience.
I hope this post helped you better understand how GSAP works and inspires you to create your own animations. Time to experiment!
Comments are moderated and will only be visible if they add to the discussion in a constructive way. If you disagree with a point, please, be polite.
Tell us what you think.