To create a polished user experience, we have to look beyond the obvious interactions. Paying close attention to cursor movements can provide valuable insights into a user’s intention.
This post explores a few interesting cursor movement techniques that use motion, pauses, and paths as meaningful signals.
Hover with intent
Sometimes users hover elements accidentally while moving their cursor across the screen. The mouseover
event is often used to determine whether an element is active and perform actions such as displaying a tooltip.
By analyzing the movement pattern, we can infer whether the user is just passing over the element or they are intentionally hovering.
Try moving your cursor over these panels. The left one activates immediately, while the right one waits to confirm your intent:
Regular Hover
Activates immediately when you hover, even if you're just passing through.
Hover with Intent
Only activates when you pause or slow down, showing deliberate interest.
Hover and scroll
This is a problem I ran into at Retool, which demonstrates how hover intent can be used in complex UIs. Consider an infinite canvas where scrolling controls the canvas viewport. Inside the canvas, we have elements that are themselves scrollable. Always passing through the scroll event to the canvas leaves us unable to scroll them:
A naive solution is to make the cards active on hover. This works for enabling scrolling, but is not ideal, as scrolling around the canvas is going to block as soon as a card is hit (try scrolling around passing over a card):
This is where hover intent comes in. We’ll make the cards active and enable scrolling only when the user intentionally hovers over them:
As you might have guessed, the trick is to wait for the cursor to stay still or slow down before firing the event listener. This is simplified implementation that illustrates the idea:
function addHoverIntent(element, onIntent, options = {}) {
const { sensitivity = 7, interval = 100 } = options;
let x = 0, y = 0, pX = 0, pY = 0;
let timer = null;
function track(e) {
x = e.clientX;
y = e.clientY;
}
function compare() {
timer = null;
// check if mouse has stayed still (within sensitivity threshold)
if (Math.abs(pX - x) + Math.abs(pY - y) < sensitivity) {
onIntent(); // user has paused - trigger intent
} else {
// mouse still moving - update position and check again
pX = x;
pY = y;
timer = setTimeout(compare, interval);
}
}
element.addEventListener('mouseenter', (e) => {
pX = e.clientX; pY = e.clientY;
element.addEventListener('mousemove', track);
timer = setTimeout(compare, interval);
});
element.addEventListener('mouseleave', () => {
if (timer) clearTimeout(timer);
element.removeEventListener('mousemove', track);
});
}
addHoverIntent(element, () => {
console.log('User showed intent!');
});
Attraction
When designing UIs, we often refer to Fitts’s law. In its basic form, Fitts’s law says that a target the user wants to hit should be bigger and closer.
Normally, the size and distance to a target are geometric facts of the interface. However, with a bit of magic, we can rewrite the geometry of the interaction itself.
Magnetism
Magnetic targets feel like they “pull” the cursor as you approach them. As the user’s intention is to move towards the target, we are making their task slightly easier.
In the context of a browser, we can’t control the cursor itself and move it towards the target. Instead, we can reduce the distance by moving the target towards the cursor:
function addMagneticEffect(buttonWrapper, button) {
const magnetRadius = 100;
const maxMove = 15;
buttonWrapper.addEventListener('mousemove', (e) => {
const rect = button.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const distX = e.clientX - centerX;
const distY = e.clientY - centerY;
const distance = Math.sqrt(distX * distX + distY * distY);
if (distance < magnetRadius) {
// Calculate magnetic pull strength (stronger when closer)
const pull = 1 - (distance / magnetRadius);
// Move button toward cursor
const moveX = (distX / distance) * maxMove * pull;
const moveY = (distY / distance) * maxMove * pull;
button.style.transform = `translate(${moveX}px, ${moveY}px)`;
}
});
buttonWrapper.addEventListener('mouseleave', () => {
// Reset position when cursor leaves
button.style.transform = '';
});
}
This is a simple example that can work in certain UIs. However, the effect can seem forceful and unexpected. A more subtle approach from the user’s perspective would be to move the cursor itself.
Warp fields
While we can’t control the actual cursor in a browser, we can create the illusion of magnetic attraction by hiding it (cursor: none
) and rendering our own. The effect is more subtle and feels like “the cursor wants to go there”:
To implement a fake cursor, we have to use a canvas, which adds some amount of complexity:
function createWarpField(canvas, target) {
const ctx = canvas.getContext('2d');
const warpRadius = 150;
const warpStrength = 0.3;
let realMouse = { x: 0, y: 0 };
let fakeCursor = { x: 0, y: 0 };
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
realMouse.x = e.clientX - rect.left;
realMouse.y = e.clientY - rect.top;
// Calculate warp effect
const targetRect = target.getBoundingClientRect();
const targetX = targetRect.left - rect.left + targetRect.width / 2;
const targetY = targetRect.top - rect.top + targetRect.height / 2;
const dx = targetX - realMouse.x;
const dy = targetY - realMouse.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < warpRadius) {
// Warp cursor toward target
const warp = (1 - distance / warpRadius) * warpStrength;
fakeCursor.x = realMouse.x + dx * warp;
fakeCursor.y = realMouse.y + dy * warp;
} else {
fakeCursor.x = realMouse.x;
fakeCursor.y = realMouse.y;
}
drawCursor();
});
function drawCursor() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw fake cursor
ctx.beginPath();
ctx.arc(fakeCursor.x, fakeCursor.y, 5, 0, Math.PI * 2);
ctx.fillStyle = 'white';
ctx.fill();
}
}
Magnetic guidelines
In design tools like Figma, alignment guides appear when elements are positioned relative to each other. Rendering the guides is a good first step, but a magnetic effect helps the user snap on the target more easily, helping users land perfectly straight layouts:
Repulsion
Magnetic attraction helps align elements, but sometimes we want the opposite effect: repulsion that prevents overlapping.
For example, we might not want these cards to overlap. We can nudge the user to avoid overlapping them as they move closer:
Safe triangles
When navigating nested menus, users often move their cursor diagonally toward a submenu item. The submenu can close prematurely when the cursor briefly exits the menu area. To illustrate this, try moving your cursor from a menu item to its submenu in this naive implementation. The submenu closes as soon as your cursor leaves the parent item:
Safe triangles create an invisible triangular zone that keeps the menu open during this natural movement pattern. With safe triangles, the menu stays open when your cursor moves through the invisible triangular zone toward the submenu:
Conclusion
Hover intent uses time spent on a target to infer whether the user actually meant to hit it.
Magnetism and warping bend the interaction field so targets feel easier to hit, while guidelines and repulsion nudge the user towards neat alignments.
Safe triangles read the trajectory of the cursor to keep submenus open.
Together, these techniques illustrate a different way to think about UI: not just reacting to explicit commands, but inferring (and shaping) user intention through motion itself.