How to Create a Button That Follows Your Mouse in NextJS + TypeScript + GSAP
Initial Variables
we firstly need the coordinates of the x and y of the mouse that is relative to the button. x
and y
are state variables that have been specifically designated to store the current position of the mouse cursor in relation to the button on the screen. However, in instances where no specific position data is available or relevant, they can also hold a value of null
. This feature of being able to store a null value becomes particularly useful when the mouse is not in motion or not interacting with the specific button, thereby giving a clear indication of a lack of positional data.
export default function component() {
const [x, setX] = useState<number | null>(null);
const [y, setY] = useState<number | null>(null);
const [mouseLeft, setMouseLeft] = useState(false);
const strength = 80;
const textStrength = 70;
return (
// rest of the code
)}
x
,y
: State variables to store the current mouse position relative to the button. They can be either numbers ornull
.mouseLeft
: A boolean state variable that tracks whether the mouse has left the button area.strength
andtextStrength
will be used for how strong you want the button to follow the cursor. (you can test out the results for yourself)
UseEffect
Create a useEffect that contains all the related animations will be inside that block:
useEffect(() => {
...
}, [x, y, mouseLeft]);
The JSX
return (
<main className="flex items-center justify-center h-screen">
<button
...
className="button flex justify-center items-center p-20 rounded-full bg-gray-700"
onMouseMove={(e) => {...}}
onMouseLeave={() => {...}}
>
<span className="text text-white font-bold">submit</span>
</button>
</main>
);
DOM Element References
Reference the elements using the class names we gave the button and the span (the text).
const button = document.querySelector(".button") as HTMLElement | null;
const text = document.querySelector(".text") as HTMLElement | null;
onMouseMove
To start with the animation, we must first see if the mouse is hovering over the button. Once it is, for every movement of the mouse inside the button will set the x
and y
and mouseLeft
will be set to false (indicating that the cursor is inside the button).
onMouseMove={(e) => {
setMouseLeft(false);
setX(e.clientX);
setY(e.clientY);
}}
onMouseLeave
onMouseLeave
resets the mouse position states and flags that the mouse has left the button.
onMouseLeave={() => {
setMouseLeft(true);
setX(null);
setY(null);
}}
Inside the UseEffect
useEffect(() => {
const button = document.querySelector(".button") as HTMLElement | null;
const text = document.querySelector(".text") as HTMLElement | null;
const boundBox = button?.getBoundingClientRect();
if (boundBox && button && x && y) {
const newnewX = parseFloat(
((x - boundBox?.left) / button.offsetWidth - 0.5).toFixed(2)
);
const newnewY = parseFloat(
((y - boundBox?.top) / button.offsetHeight - 0.5).toFixed(2)
);
gsap.to(button, {
duration: 1,
x: newnewX * strength,
y: newnewY * strength,
ease: Power4.easeOut,
});
gsap.to(text, {
duration: 1,
x: newnewX * textStrength,
y: newnewY * textStrength,
ease: Power4.easeOut,
});
}
if (mouseLeft) {
gsap.to(button, {
duration: 1,
x: 0,
y: 0,
ease: "bounce",
});
gsap.to(text, {
duration: 1,
x: 0,
y: 0,
ease: "bounce",
});
}
}, [x, y, mouseLeft]);
Let’s Break it Down Shall We?
Just Kidding!
This condition checks if boundBox
, button
, x
, and y
are all defined. x
and y
are the mouse coordinates relative to the viewport. If any of these are missing, the animation won't proceed.
if (boundBox && button && x && y) {
...
}
The following lines calculate the position of the mouse relative to the center of the button. The calculations adjust the mouse coordinates (x
and y
) by subtracting the top-left corner of the button (boundBox.left
and boundBox.top
). The result is then divided by the button's width and height to normalize the value between -0.5 and 0.5, which centers around zero.
const newnewX = parseFloat(
((x - boundBox?.left) / button.offsetWidth - 0.5).toFixed(2)
);
const newnewY = parseFloat(
((y - boundBox?.top) / button.offsetHeight - 0.5).toFixed(2)
);
Afterwards we give these values to GSAP.
// animation for the button
gsap.to(button, {
duration: 1,
x: newnewX * strength,
y: newnewY * strength,
ease: Power4.easeOut,
});
// animation for the text
gsap.to(text, {
duration: 1,
x: newnewX * textStrength,
y: newnewY * textStrength,
ease: Power4.easeOut,
});
Using only the newnewX
and newnewY
values will move the button, but it will not show much. Therefore, we can multiply it by the strength
and textStrength
we created earlier.
Full Code
"use client";
import { useEffect, useState } from "react";
import gsap, { Power4 } from "gsap";
export default function Button() {
const [x, setX] = useState<number | null>(null);
const [y, setY] = useState<number | null>(null);
const [mouseLeft, setMouseLeft] = useState(false);
const strength = 80;
const textStrength = 70;
useEffect(() => {
const button = document.querySelector(".button") as HTMLElement | null;
const text = document.querySelector(".text") as HTMLElement | null;
const boundBox = button?.getBoundingClientRect();
if (boundBox && button && x && y) {
const newnewX = parseFloat(
((x - boundBox?.left) / button.offsetWidth - 0.5).toFixed(2)
);
const newnewY = parseFloat(
((y - boundBox?.top) / button.offsetHeight - 0.5).toFixed(2)
);
gsap.to(button, {
duration: 1,
x: newnewX * strength,
y: newnewY * strength,
ease: Power4.easeOut,
});
gsap.to(text, {
duration: 1,
x: newnewX * textStrength,
y: newnewY * textStrength,
ease: Power4.easeOut,
});
}
if (mouseLeft) {
gsap.to(button, {
duration: 1,
x: 0,
y: 0,
ease: "bounce",
});
gsap.to(text, {
duration: 1,
x: 0,
y: 0,
ease: "bounce",
});
}
}, [x, y, mouseLeft]);
return (
<main className="flex items-center justify-center h-screen">
<button
id="button"
className="button flex justify-center items-center p-20 rounded-full bg-gray-700"
onMouseMove={(e) => {
setMouseLeft(false);
setX(e.clientX);
setY(e.clientY);
}}
onMouseLeave={() => {
setMouseLeft(true);
setX(null);
setY(null);
}}
>
<span className="text text-white font-bold">submit</span>
</button>
</main>
);
}
And There You Have it!
I hope you found this article both helpful and entertaining. Whether you're just starting out with React and GSAP or looking to polish up your skills, I trust there's something in this breakdown that sparked a bit of inspiration or added a new tool to your developer toolkit.
Happy coding :)