LANARS

How to create an Animated cursor pointer in React

How to create an Animated cursor pointer in React
Time to read
17 min
Section
Share
Subscribe
This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

INTRODUCTION

In this tutorial, we'll walk through the process of creating an animated cursor pointer in a React application. Animated cursor pointer can add a unique and interactive element to your website, enhancing the user experience. While we'll rely on React and the Framer Motion library to implement this effect, the primary enchantment will come from straightforward CSS.

Project init, Basic components

To start, we'll need to install Vite and Framer Motion for our new project.

We're ready to begin creating the UI components, starting with the layout.

/* src/App.tsx */

function App() {
  return (
    <section className="section">
      <h1>
        Animated Cursor <br />
        React Component
      </h1>
    </section>
  );
}

export default App;

 

Add styles for section

/* src/index.css */

.section {   
  display: flex;
  flex-direction: column;
  align-items: center;
  margin: 0 20px;
}

 

Create simple button

/* src/components/Button/index.tsx */

import './Button.css';

const Button = () => (
    <button className="btn btn--circle">Click</button>
);

export default Button;

 

Add styles

/* src/components/Button/Button.css */

.btn {
  font-family: var(--main-font-family);
  font-size: 1em;
  z-index: 9;
  position: relative;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}

.btn--circle {
  width: 48px;
  height: 48px;
  padding: 5px;
  background-color: transparent;
  color: var(--main-light-color);
  border-radius: 50%;
  border: 1px solid var(--main-light-color);
  transition: color 300ms ease-in-out, border 300ms ease-in-out;
}

 

To integrate global styles into your React app, follow these steps.

Define your CSS variables at the beginning of the file using the :root selector

/* src/index.css */

:root {
  --main-dark-color: #212121;
  --main-light-color: #fff;
  --secondary-color: #ed5517;
  --main-font-family: 'Inter', sans-serif;
  --cursor-border-radius: 50%;
  --cursor-transition: 300ms ease;
  --cursor-width: 12px;

  font-synthesis: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

 

Set global styles for elements like *, after, before and other

/* src/index.css */

*,
after,
before {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html,
body {
  background-color: var(--main-dark-color);
  color: var(--main-light-color);
  font-family: var(--main-font-family);
}

Create the Magnetic component to apply the magnetic effect to the button

The MagneticFramer component is designed to create a magnetic effect on the cursor pointer when you hover over its children. Here's a breakdown of the logic:

The component uses useState to manage the position of the cursor pointer and useRef to reference the component's DOM element.

/* src/components/MagneticFramer.tsx */

import React, { ReactNode, useRef, useState } from 'react';
import { motion } from 'framer-motion';

const MagneticFramer = ({ children }: { children: ReactNode }) => {
  const ref = useRef<null | HTMLDivElement>(null);
  const [position, setPosition] = useState({ x: 0, y: 0 });

  const { x, y } = position;

  return (
    <motion.div
      ref={ref}
      animate={{ x, y }}
      className="magnetic-framer"
    >
      {children}
    </motion.div>
  );

 

The handleMouse function calculates the position of the cursor pointer relative to the center of the MagneticFramer component and updates the position state accordingly.

By calculating the offset of the cursor pointer from the element's center along both axes (offsetX and offsetY) and then normalizing these values by dividing them by half the element's width and height, respectively, we get values that show the position of the cursor pointer relative to the element's center in the range from -1 to 1.

Multiply the normalized values by 5 to increase the effect of moving the cursor pointer. The resulting values (middleX and middleY) are used to determine how much the cursor pointer should be moved on the plane.

/* src/components/MagneticFramer.tsx */

const handleMouse = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
  const { clientX, clientY } = e;
  const { height, width, left, top } = ref.current!.getBoundingClientRect();
  const offsetClientX = left + width / 2;
  const offsetClientY = top + height / 2;
  const normalizedX = offsetClientX / (width / 2);
  const normalizedY = offsetClientY / (height / 2);
  const middleX = normalizedX * 5;
  const middleY = normalizedY * 5;

  setPosition({ x: middleX, y: middleY });

  return (
    <motion.div
       /* ... */
       onMouseMove={handleMouse}
    >
      {children}
    </motion.div>
  );

 

The reset function resets the position of the cursor pointer when the mouse leaves the MagneticFramer component.

/* src/components/MagneticFramer.tsx */
//...

const reset = () => {
  setPosition({ x: 0, y: 0 });
};

  return (
    <motion.div
      /* ... */
      onMouseLeave={reset}
    >
      {children}
    </motion.div>
  );

 

The MagneticFramer component is used in the Button component to apply the magnetic effect to a button.

/* src/components/Button/index.tsx */

import MagneticFramer from '../MagneticFramer';
import './Button.css';

const Button = () => (
  <MagneticFramer>
    <button className="btn btn--circle">Click</button>
  </MagneticFramer>
);

export default Button;

 

We're using a motion component to wrap the content of the component from the ‘framer-motion’ library.

Transition configuration specifies a spring-based animation with the following parameters:

  • type: Specifies the type of animation. In this case, it's set to 'spring', indicating a spring-based animation.
  • stiffness: Determines how stiff the spring is. Higher values result in a stiffer spring and faster initial movement.
  • damping: Controls how much the spring oscillates around its resting position. Higher values dampen the oscillations more quickly.
  • mass: Represents the mass of the object attached to the spring. Higher values will result in more lethargic movement.

In summary, this transition configuration creates a spring-like animation that is moderately stiff (stiffness: 150) with some damping (damping: 50) and a relatively light mass (mass: 0.2). Adjusting these parameters can change the feel and behavior of the animation.

/* src/components/MagneticFramer.tsx */

/* ... */
return (
    <motion.div
       /* ... */
      transition={{ type: 'spring', stiffness: 150, damping: 50, mass: 0.2 }}
    >
      {children}
    </motion.div>
);

 

Overall, the MagneticFramer component provides a reusable way to add a magnetic cursor pointer effect to elements in a React application, enhancing the user experience with interactive and engaging UI elements.

Create Animated cursor pointer

The AnimatedCursor component is responsible for creating and managing the animated cursor pointer effect on the webpage. let's take a closer look at this component:

1. Initialization

The useRef hook is employed to establish references for the cursor pointer dot element (cursorDot), the requestAnimationFrame ID (requestRef), and the previous timestamp (previousTimeRef).

The useState hook is utilized to handle the mouse position (mousePosition), window dimensions (width and height), as well as cursor pointer visibility (cursorVisible and cursorEnlarged).

/* src/components/AnimatedCursor/index.tsx */

import { useState, useEffect, useRef } from 'react';
import './AnimatedCursor.css';

const AnimatedCursor = () => {
  const cursorDot = useRef<null | HTMLDivElement>(null);
  const requestRef = useRef<number>(0);
  const previousTimeRef = useRef<number>(0);

  const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
  const [width, setWidth] = useState<number>(window.innerWidth);
  const [height, setHeight] = useState<number>(window.innerHeight);

  const cursorVisible = useRef(false);
  const cursorEnlarged = useRef(false);
/* ... */

2. Mouse Movement Handling

onMouseMove function updates the mousePosition state and calls positionDot to update the position of the cursor pointer.

const onMouseMove = (event: MouseEvent) => {
    const { pageX: x, pageY: y } = event;
    setMousePosition({ x, y });
    positionDot(event);
};

 

onMouseLeave function hides the cursor pointer when the mouse leaves the document.

const onMouseLeave = () => {
  cursorVisible.current = false;
  toggleCursorVisibility();
};

 

positionDot calculates the new cursor pointer position based on the mouse position and updates the cursor pointer style accordingly.

let { x, y } = mousePosition;
const winDimensions = { width, height };
let endX = winDimensions.width / 2;
let endY = winDimensions.height / 2;

const positionDot = (e: MouseEvent) => {
  if (!cursorEnlarged.current) {
    cursorVisible.current = true;
    toggleCursorVisibility();
    endX = e.pageX;
    endY = e.pageY;

    cursorDot.current!.style.top = endY + 'px';
    cursorDot.current!.style.left = endX + 'px';
  }
};

3. Cursor Pointer Animation

animateDot uses requestAnimationFrame to smoothly animate the cursor pointer towards the target position (endX and endY).

const animateDot = (time: number) => {
  if (previousTimeRef.current !== undefined && !cursorEnlarged.current) {
    x += (endX - x) / 2;
    y += (endY - y) / 2;
    cursorDot.current!.style.top = y  + 'px';
    cursorDot.current!.style.left = x + 'px';
  }
    previousTimeRef.current = time;
    requestRef.current = requestAnimationFrame(animateDot);
};

 

4. Cursor Pointer Visibility and Size:

toggleCursorVisibility toggles the cursor pointer visibility based on the cursorVisible state.

const toggleCursorVisibility = () => {
  if (cursorVisible.current) {
    cursorDot.current!.style.opacity = '1';
  } else {
    cursorDot.current!.style.opacity = '0';
    cursorDot.current!.classList.remove('active');
  }
};

 

toggleCursorSize adjusts the cursor pointer size and position based on the element's bounding box when the cursor pointer is enlarged.

const toggleCursorSize = (el: HTMLButtonElement | Element) => {
  if (cursorEnlarged.current && el) {
    const { width, height, top, left } = el!.getBoundingClientRect();
    const centerX = left + width / 2;
    const centerY = top + height / 2;
    cursorDot.current!.className = 'active';
    cursorDot.current!.style.top = centerY + 'px';
    cursorDot.current!.style.left = centerX + 'px';
  } else {
     cursorDot.current!.classList.remove('active');
     cursorDot.current!.style.top = '0px';
     cursorDot.current!.style.left = '0px';
  }
};

5. Event Listeners

useEffect is used to add event listeners for mousemove, mouseleave, and resize events. These event listeners are used to track the mouse movement, detect when the mouse leaves the window, and update the window dimensions, respectively. The useEffect hook returns a cleanup function that removes the event listeners and cancels the animation frame request when the component is unmounted. This prevents memory leaks and ensures that resources are properly released.

useEffect(() => {
  document.addEventListener('mousemove', onMouseMove);
  document.addEventListener('mouseleave', onMouseLeave);
  window.addEventListener('resize', onResize);
  requestRef.current = requestAnimationFrame(animateDot);

  handleLinkHovers();

  return () => {
    document.removeEventListener('mousemove', onMouseMove);
    document.removeEventListener('mouseleave', onMouseLeave);
    window.removeEventListener('resize', onResize);
    cancelAnimationFrame(requestRef.current);
  };
}, []);

 

handleLinkHovers function within the AnimatedCursor component is in charge of attaching event listeners to all elements with the .btn class, allowing the detection of when the cursor pointer hovers over them. This function plays a vital role in implementing interactive cursor pointer effects, like enlarging the cursor pointer when hovering over specific elements and changing the background color when you click on a button.

const handleLinkHovers = () => {
  document.querySelectorAll('.btn').forEach((el) => {
     el.addEventListener('mousemove', () => {
     cursorEnlarged.current = true;
     toggleCursorSize(el);
    });
     el.addEventListener('mouseleave', () => {
     cursorEnlarged.current = false;
     toggleCursorSize(el);
    });
  el.addEventListener('click', () => {
     cursorDot.current!.classList.add('active-click');
     setTimeout(() => {
     cursorDot.current!.classList.remove('active-click');
   }, 300);
  });
 });
};

 

The AnimatedCursor component returns a div element with a ref to cursorDot and the id cursor-dot.

/* src/components/AnimatedCursor/index.tsx */

import { useState, useEffect, useRef } from 'react';
import './AnimatedCursor.css';

const AnimatedCursor = () => {
/* ... */
  return <div ref={cursorDot} id="cursor-dot" />;
/* ... */

6. Add styles for AnimatedCursor

Explanation of some points in styles:

- The opacity is set to 0 by default and transitions to 1 for a fade-in effect.

/* src/components/AnimatedCursor/AnimatedCursor.css */

#cursor-dot {
  z-index: 9;
  pointer-events: none;
  position: absolute;
  top: 50%;
  left: 50%;
  opacity: 0;
  will-change: auto;
  rotate: none;
  scale: none;
  width: var(--cursor-width);
  height: var(--cursor-width);
  border-radius: var(--cursor-border-radius);
  transform-origin: 0 0;
  transition: opacity 500ms ease-in-out, background 400ms ease-in-out;
}

 

The before pseudo-element is used to create a larger circle around the cursor pointer for the hover effect.

#cursor-dot:before {
  content: '';
  position: absolute;
  top: -24px;
  left: -24px;
  display: block;
  width: calc(var(--cursor-width) * 4);
  height: calc(var(--cursor-width) * 4);
  transform: scale(0.2);
  background: var(--main-light-color);
  border-radius: var(--cursor-border-radius);
  transition: all var(--cursor-transition);
}

 

When the cursor pointer is enlarged (cursorEnlarged is true), the width and height of the cursor pointer point are increased, and the before pseudo-element transform is scaled to 1.3.

#cursor-dot.active:before {
  transform: scale(1.3) !important;
}

 

This CSS rule is applied to the cursor pointer element when a button is clicked. It changes the background color of the cursor pointer to #ed5517 color and reduces its scale.

#cursor-dot.active-click:before {
  background: var(--secondary-color);
  transform: scale(1.2) !important;
}

7. Adjust the scale in the MagneticFramer component

We also adjust the scale of the cursor pointer depending on its position in the MagneticFramer component.

The handleMouse function updates the position of the cursor pointer and scale by directly manipulating its transform property. We calculate the rotation angle using Math.atan2() to get the angle between the position of the cursor pointer and the center of the element. Then updates the transform property of the cursor pointer element's style to translate the cursor pointer to the new position and rotate it based on the calculated angle.

/* src/components/MagneticFramer.tsx */

const handleMouse = (e: React.MouseEvent<HTMLDivElement, MouseEvent>)   => {
  /* ... */
  const angle = -Math.atan2(offsetClientX, offsetClientY);

  if (cursor) {
    cursor.style.willChange = 'transform';
    cursor.style.transform = `translate(${middleX}px, ${middleY}px) rotate(${angle}rad)`;
   }
 };
};

 

Additionally, it adjusts the scale of the cursor pointer based on its position relative to the center, making it slightly smaller as it moves away from the center, to create a visual effect.

The distance variable in the handleMouse function of the MagneticFramer component represents the Euclidean distance between the current mouse position and the center of the MagneticFramer component. It is calculated using the Pythagorean theorem:

  • centerX and centerY are the horizontal and vertical center points of the MagneticFramer component, respectively.
  • clientX and clientY are the current horizontal and vertical coordinates of the mouse pointer, respectively.

This distance is used to determine the scale factor applied to the cursor pointer. As the cursor pointer moves closer to the center of the MagneticFramer component, the scale factor decreases, making the cursor pointer appear smaller. Conversely, as the cursor pointer moves away from the center, the scale factor increases, making the cursor pointer appear larger. This effect adds a dynamic element to the size of the cursor pointer, enhancing the overall user experience.

It also sets the willChange property to transform to optimize browser rendering.

/* src/components/MagneticFramer.tsx */

const handleMouse = (e: React.MouseEvent<HTMLDivElement, MouseEvent>)   => {
  /* ... */
  const dx = Math.pow(centerX - clientX, 2);
  const dy = Math.pow(centerY - clientY, 2);
  const distance = Math.sqrt(dx + dy);

  if (cursor) {
    cursor.style.transform += `scale(${1 - distance * 0.005}, 1)`;
   }
  };
};

 

The reset function resets the position of the cursor pointer and scale when the mouse leaves the MagneticFramer component.

/* src/components/MagneticFramer.tsx */

const reset = () => {
  setPosition({ x: 0, y: 0 });

  if (cursor) {
    cursor!.style.transform = '';
    cursor!.style.willChange = 'auto';
  }
};

 

Finally, render the The AnimatedCursor component in our App.tsx along with other content.

/* src/App.tsx */
//...

function App() {
  return (
    <>
        /* ... */
       <AnimatedCursor />
    </>
  );
}

export default App;

 

Summary

In this guide, we've covered the process of implementing an animated cursor pointer in a React application using the Framer Motion library.

We developed a Cursor pointer component that changes its shape and size depending on the position of the cursor pointer and applied CSS styling to it. Finally, we've integrated our custom animated Cursor component into the App component and added CSS for interactivity.

Animated cursor pointer can be tailored and expanded upon to suit the requirements of your project, contributing a playful and engaging aspect to your website.

You can check out the final result here. And don't forget to visit our project's page on GitHub.