Awesome animated cursor with React Hooks⚡️
Tags: react, javascript, reacthooks, cursor
Published: 2020-06-01 12:36:24

Don't you find built-in cursors kinda boring?🥱 Me too. So I built my own.

---

Let's start by adding basic styles and logic to our cursor.

```css
.cursor {
  width: 40px;
  height: 40px;
  border: 2px solid #fefefe;
  border-radius: 100%;
  position: fixed;
  transform: translate(-50%, -50%);
  pointer-events: none;
  z-index: 9999;
  mix-blend-mode: difference;
}

html, body {
  cursor: none;
  background-color: #121212;
}
```

```javascript
const Cursor = () => {
    return <div className="cursor"/>
}

ReactDOM.render(
    <div className="App">
        <Cursor/>
    </div>,
    document.getElementById('root')
);
```

Now we want to change our cursor's position based on mouse moves.

```diff
const Cursor = () => {
+   const [position, setPosition] = useState({x: 0, y: 0});
+
+   useEffect(() => {
+       addEventListeners();
+       return () => removeEventListeners();
+   }, []);
+
+   const addEventListeners = () => {
+       document.addEventListener("mousemove", onMouseMove);
+   };
+
+   const removeEventListeners = () => {
+       document.removeEventListener("mousemove", onMouseMove);
+   };
+
+   const onMouseMove = (e) => {
+       setPosition({x: e.clientX, y: e.clientY});
+   };                                                               
+
-   return <div className="cursor"/>
+   return <div className="cursor"
+           style={{
+               left: `${position.x}px`,
+               top: `${position.y}px`
+           }}/>
}

...
```

When a component is mounted we add an event listener that handles `mousemove` event and remove it when the component is going to unmount. In `onMouseMove` function we set new cursor's position based on `e.clientX` and `e.clientY` properties.

![Cursor GIF](https://i.gyazo.com/885173cc0bbd64933e669ba4884698f2.gif)

Now our cursor reacts to mouse moves, but as you can see it doesn't hide when the mouse leaves the screen. So let's fix it!

```diff
.cursor {
  ...
+ transition: all 150ms ease;
+ transition-property: opacity;
}

+ .cursor--hidden {
+   opacity: 0;
+ }

...
```

```diff
+ import classNames from "classnames";

const Cursor = () => {
    const [position, setPosition] = useState({x: 0, y: 0});
+   const [hidden, setHidden] = useState(false);

...

    const addEventListeners = () => {
        document.addEventListener("mousemove", onMouseMove);
+       document.addEventListener("mouseenter", onMouseEnter);
+       document.addEventListener("mouseleave", onMouseLeave);
    };

    const removeEventListeners = () => {
        document.removeEventListener("mousemove", onMouseMove);
+       document.removeEventListener("mouseenter", onMouseEnter);
+       document.removeEventListener("mouseleave", onMouseLeave);
    };
+
+   const onMouseLeave = () => {
+       setHidden(true);
+   };
+
+   const onMouseEnter = () => {
+       setHidden(false);
+   };
    ...
+
+   const cursorClasses = classNames(
+       'cursor',
+       {
+           'cursor--hidden': hidden
+       }
+   );                                                             
+
-   return <div className="cursor"
+   return <div className={cursorClasses}
            style={{
                left: `${position.x}px`,
                top: `${position.y}px`
            }}/>
}

...
```

So, I add `mouseleave` and `mouseenter` handler. When the mouse enters the screen's `opacity` becomes `1` and when leaves - equals to `0`. Additionally, I add `classnames` library which is a simple utility for conditionally joining classNames together.

![Cursor GIF](https://i.gyazo.com/ce34c4738130e0825c6857ad7c59bc6e.gif)

Now it looks way better, but let's add some more stuff! 

Let's add click animation.
```diff
.cursor {
  ...
- transition-property: opacity;
+ transition-property:  opacity, background-color, transform, mix-blend-mode;
  ...
}

+ .cursor--clicked {
+   transform: translate(-50%, -50%) scale(0.9);
+   background-color: #fefefe;
+ }

...
```

```diff
const Cursor = () => {
    ...
+   const [clicked, setClicked] = useState(false);
    
    const addEventListeners = () => {
        ...
+       document.addEventListener("mousedown", onMouseDown);
+       document.addEventListener("mouseup", onMouseUp);
    };

    const removeEventListeners = () => {
        ...
+       document.removeEventListener("mousedown", onMouseDown);
+       document.removeEventListener("mouseup", onMouseUp);
    };
+
+   const onMouseDown = () => {
+       setClicked(true);
+   };
+
+   const onMouseUp = () => {
+       setClicked(false);
+   };

    ...

    const cursorClasses = classNames(
        'cursor',
        {
+           'cursor--clicked': clicked,
            'cursor--hidden': hidden
        }
    );

...
```

Mouse clicks are handled by `mousedown` and `mouseup` event. When the mouse is clicked, the cursor's scale changes to `0.9` and background to `#fefefe`.

![Cursor GIF](https://i.gyazo.com/7032996b0b10b969c8d91845e6e02d6f.gif)

Let's move on to our final animation!

Now we will add some effects when links have hovered.

```diff
...

+ .cursor--link-hovered {
+   transform: translate(-50%, -50%) scale(1.25);
+   background-color: #fefefe;
+ }
+
+ a {
+   text-decoration: underline;
+   color: #fefefe;
+ }

...
```

```diff
const Cursor = () => {
    ...
+   const [linkHovered, setLinkHovered] = useState(false);
    
    useEffect(() => {
       addEventListeners();
+      handleLinkHoverEvents();
       return () => removeEventListeners();
    }, []);
+   
    ...
+
+   const handleLinkHoverEvents = () => {
+       document.querySelectorAll("a").forEach(el => {
+           el.addEventListener("mouseover", () => setLinkHovered(true));
+           el.addEventListener("mouseout", () => setLinkHovered(false));
+       });
+   };

    const cursorClasses = classNames(
        'cursor',
        {
            'cursor--clicked': clicked,
            'cursor--hidden': hidden,
+           'cursor--link-hovered': linkHovered
        }
    );
    ...
}

ReactDOM.render(
    <div className="App">
+       <a>This is a link</a>
        <Cursor/>
    </div>,
    document.getElementById('root')
);

```

When a component is mounted, `handleLinkHoverEvents` add event listeners to all link elements. When a link hovers, `cursor--link-hovered` class is added.

![Cursor GIF](https://i.gyazo.com/738c48ba452f32d38d80df26816fa03c.gif)

In the final step, we will not render `<Cursor/>` on mobile/touch devices.

```diff
+ const isMobile = () => {
+     const ua = navigator.userAgent;
+     return /Android|Mobi/i.test(ua);
+ };

const Cursor = () => {
+   if (typeof navigator !== 'undefined' && isMobile()) return null;
    ...
}

...
```

And we are done! Here is a full codepen example:

{% codepen https://codepen.io/andrewchmr-the-vuer/pen/GRZjbbB %}

---

Adding custom cursor animation is not as difficult as it seems to be. I hope that this article will give you a basic idea of what you can do to customize your own cursor.

Thanks for reading!