Skip to main content

How to Create a Button That Follows Your Mouse in NextJS + TypeScript + GSAP

· 6 min read
Suha Karkhy Mohammed
Software Engineer

screen-capture (online-video-cutter.com) - Trim.gif

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 or null.
  • mouseLeft: A boolean state variable that tracks whether the mouse has left the button area.
  • strength and textStrength 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?

breakitdown.gif

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 :)