Custom Dynamic Cursor in React

Custom Dynamic Cursor in React cover image

Category: WebDev

Posted at: Sep 3, 2024

10 Minutes Read

In this tutorial, we'll build a dynamic cursor that changes whenever you hover over different elements in React. You can see a demo on my personal website https://husainm.com


I tried many different ways, this method is the smoothest and easiest.

Mouse tracker function

First, let's create a state variable to store the current mouse coordinates:

const useMousePosition = () => {
  const [mousePosition, setMousePosition] = useState({ x: null, y: null });
};


Next, we'll use useEffect because it runs after the component has rendered, so we can set the event listeners when the component is ready. Inside it, we'll create a function that updates the mouse coordinates, which we'll use in a window event listener. The event we'll listen for is mousemove, which will trigger the function whenever the mouse moves:

const useMousePosition = () => {
  const [mousePosition, setMousePosition] = useState({ x: null, y: null });

  useEffect(() => {
    const updateMousePosition = (ev) => {
      setMousePosition({ x: ev.clientX, y: ev.clientY });
    };

    window.addEventListener("mousemove", updateMousePosition);
  }, []);
};


Third, we'll clean up the event listener and return the mouse coordinates so we can use them.

What is clean up? when we add an event listener to the 'window', it stays active and keeps listening for events, even if the component is longer visible. This might cause issues, so it's good practice to remove the event listener when the component is unmounted.
const useMousePosition = () => {
  const [mousePosition, setMousePosition] = useState({ x: null, y: null });


  useEffect(() => {
    const updateMousePosition = (ev) => {
      setMousePosition({ x: ev.clientX, y: ev.clientY });
    };


    window.addEventListener("mousemove", updateMousePosition);

    // cleanup
    return () => {
      window.removeEventListener("mousemove", updateMousePosition);
    };
  }, []);


  return mousePosition;
}; 

Custom Cursor

Now, we have a function that provides the mouse coordinates, and now it's the time to use it on our page.

We can use the useMousePosition function and store its result in a variable named mousePosition:

export default function Page(){
  const mousePosition = useMousePosition();

  return (
    <div className="page">
      <div className="pointer" />
      <div className="card">
        // content
      </div>
    </div>
  )
}

In this scenario we'll use the .pointer div as the custom cursor. You can style it however you like, make it a square, circle, or triangle, and give it a background color.


Here's a recommended styling:

.pointer {
  pointer-events: none;
  position: fixed;
  z-index: 1000;
  
  /* Centering the element */
  transform-origin: center;
  transform: translate(-50%,-50%);
}

.pointer__default {
  background-color: #ddd;
  width: 30px;
  height: 30px;
  border-radius: 30px;
}

/* hide the cursor in touch screens */
@media only screen and (hover: none) {
  .pointer {
    display: none; 
  }
}


Next, let's replace the default cursor with our custom one. First, we'll hide the default cursor using the following CSS code:

*,
*::before,
*::after {
  cursor: none !important;
}


Then, update the position of the .pointer element based on the mouse's position:

<div
className="pointer"
style={{
  left: `${mousePosition.x}px`,
  top: `${mousePosition.y}px`,
}}
/>

And there you have it a custom cursor. Now, let's make it dynamic.

Dynamic Cursor

Now, we will make the cursor changes whenever we hovers over an element.


We'll begin by declaring a state variable to store the area we are in or which element we are hovering over, with an initial value default. Style the class .pointer__default for your main cursor. Then, we will use the value of the area variable to style the pointer based on the area. For example, you can have classes like .pointer__header and .pointer__card, giving each class its own unique styling:

export default function Page() {
  const mousePosition = useMousePosition();
  const [area, setArea] = useState("default");

  return (
    <div className="page">
      <div
        className={`pointer pointer__${area}`}
        style={{
          left: `${mousePosition.x}px`,
          top: `${mousePosition.y}px`,
        }}
      />
      <div className="card"></div>
    </div>
  );
}


Styling for .pointer__card:

.pointer__card {
  background-color: #b62929;
  width: 4rem;
  height: 4rem;
  border-radius: 10rem;
}


Next, I will use the onMouseEnter event listener to change the value of the area variable, allowing us to apply different styling when we hover over an element. In our case, this will be the card element.

export default function Page() {
  const mousePosition = useMousePosition();
  const [area, setArea] = useState("default");

  return (
    <div className="page">
      <div
        className={`pointer pointer__${area}`}
        style={{
          left: `${mousePosition.x}px`,
          top: `${mousePosition.y}px`,
        }}
      />
      <div className="card" onMouseEnter={() => setArea("card")}></div>
    </div>
  );
}


The problem now is when you hover over the element, the cursor will change, but it won't revert to its original styling when you leave the element. To solve this, we will create a function that resets the area back to the default value and use it with the onMouseLeave event:

export default function Page() {
  const mousePosition = useMousePosition();
  const [area, setArea] = useState("default");

  function resetPointer() {
    setArea("default");
  }

  return (
    <div className="page">
      <div
        className={`pointer pointer__${area}`}
        style={{
          left: `${mousePosition.x}px`,
          top: `${mousePosition.y}px`,
        }}
      />
      <div
        className="card"
        onMouseEnter={() => setArea("card")}
        onMouseLeave={resetPointer}
      ></div>
    </div>
  );
}

You might wonder why I didn't simply use onMouseLeave={() => setArea("default")}. The reason is that you may want to perform additional actions when resetting the pointer.


Here is the final version:

import { useEffect, useState } from "react";

const useMousePosition = () => {
  const [mousePosition, setMousePosition] = useState({ x: null, y: null });

  useEffect(() => {
    const updateMousePosition = (event) => {
      setMousePosition({ x: event.clientX, y: event.clientY });
    };

    window.addEventListener("mousemove", updateMousePosition);

    return () => {
      window.removeEventListener("mousemove", updateMousePosition);
    };
  }, []);

  return mousePosition;
};

export default function Page() {
  const mousePosition = useMousePosition();
  const [area, setArea] = useState("default");

  function resetPointer() {
    setArea("default");
  }

  return (
    <div className="page">
      <div
        className={`pointer pointer__${area}`}
        style={{
          left: `${mousePosition.x}px`,
          top: `${mousePosition.y}px`,
        }}
      />
      <div
        className="card"
        onMouseEnter={() => setArea("card")}
        onMouseLeave={resetPointer}
      ></div>
    </div>
  );
}


Good luck!