How to make an advanced pointer animation (TS React and Framer Motion)
Tags: tutorial, webdev, react, css
Published: 2022-09-08 08:05:44

---
title: How to make an advanced pointer animation (TS React and Framer Motion)
published: true
description: Recreating an advanced Framer Motion animation in React and TypeScript 
tags: tutorial,webdev,react,css
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oq1yvn0d9fd7jn2zz7oj.gif
# cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kz8ru4o43uhaz24a7ctt.gif
# Use a ratio of 100:42 for best results.
---

I found the [Pointer blog](https://blog.pointer.gg/) through Product Hunt, and I was really impressed with the background animation there. It's intricate, but because it only appears when the mouse is moved it's still low key and minimal.

I wanted to see how it was made, so I experimented and tried to reverse engineer it. This is what we'll be building today: [demo](https://pointer-animation.netlify.app/).

I learnt a lot about Framer Motion along the way, and finally got a chance to try out Vite. It's fast.

I figured this is a great opportunity to pass on the knowledge. Here are some of the things you'll learn:

- React
- Vite
- TypeScript
- Emotion for CSS-in-JS
- Advanced animation with Framer Motion

---

## Setup
We'll start by scaffolding our project with Vite:

```shell
# npm
npm create vite@latest pointer-animation -- --template react-ts

# yarn
yarn create vite pointer-animation --template react-ts
```

Follow the commands at the the end to install dependencies and run the project in development mode, then install some further dependencies:

```shell
# npm
npm install @emotion/react @emotion/styled framer-motion

# yarn
yarn add @emotion/react @emotion/styled framer-motion
```

## The grid

Create a `components` folder inside of `src` where we can start to build our components, and inside that create a `Cell.tsx` component with the following:

```tsx
import styled from '@emotion/styled';

export const CELL_SIZE = 60;

const Container = styled.div`
  width: ${CELL_SIZE}px;
  height: ${CELL_SIZE}px;
  border: 1px dashed #555;
  color: #777;
  display: flex;
  justify-content: center;
  align-items: center;
  user-select: none;
`;

const Cell: React.FC = () => {
  return <Container>→</Container>;
};

export default Cell;

```

This is our grid cell with some basic styling. Notice that there is a text `→` that we will animate later. The `CELL_SIZE` is a constant at the top of the file so we can easily adjust it.

Next up, let's create a `Grid.tsx` component:

```tsx
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import { useEffect, useState } from 'react';
import Cell, { CELL_SIZE } from './Cell';

const Container = styled(motion.div)<{
  columns: number;
}>`
  position: absolute;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  overflow: hidden;
  display: grid;
  grid-template-columns: repeat(${(props) => props.columns}, 1fr);
`;

function Grid() {
  const [columns, setColumns] = useState(0);
  const [rows, setRows] = useState(0);

  // determine rows and columns
  useEffect(() => {
    const calculateGrid = () => {
      const columnCount = Math.ceil(window.innerWidth / CELL_SIZE);
      setColumns(columnCount);
      const rowCount = Math.ceil(window.innerHeight / CELL_SIZE);
      setRows(rowCount);
    };
    // calculate the grid on load
    calculateGrid();
    // recalculate grid on resize
    window.addEventListener('resize', calculateGrid);
    // cleanup
    return () => {
      window.removeEventListener('resize', calculateGrid);
    };
  }, []);

  return (
    <Container columns={columns}>
      {Array.from({ length: columns * rows }).map((_, i) => (
        <Cell key={i} />
      ))}
    </Container>
  );
}

export default Grid;

```

Let's break this down.

We style our `Container` element so that it covers the entire viewport (`100vw` and `100vw`) and is absolutely positioned to the top left.

We use CSS grid here, and determine the number of columns based on a prop from the main `Grid` component.

The `Grid` component holds the number of columns and rows in state. That's calculated inside the `useEffect` which runs when the component mounts.

The `calculateGrid` function determines the number of columns and rows we'll need to cover the entire screen with `Cell`s. We call this once (so it runs when the component mounts) and add it to an event listener. If the user resizes the screen, the number of rows and columns will be recalculated.

Finally, we render `Cell` components based on the number of columns and rows with a little trick using `Array.from`.

Let's remove some boilerplate and render the `Grid` component inside of `App.tsx`:
```tsx
import './App.css';
import Grid from './components/Grid';

function App() {
  return (
    <Grid />
  );
}

export default App;

```

We should now have a grid that responds to the user's browser width!

![Static arrows](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wa8ay7968m8b7xuwdqtx.png)

## Pointers

We have our static grid and arrows, and now we want them to point towards the mouse cursor. 

Add the following to the `Grid` component:

```jsx
import { animate, motion, useMotionValue } from 'framer-motion';

// ...

// mouse position
const mouseX = useMotionValue(0);
const mouseY = useMotionValue(0);

// handle mouse move on document
useEffect(() => {
	const handleMouseMove = (e: MouseEvent) => {
		// animate mouse x and y
		animate(mouseX, e.clientX);
		animate(mouseY, e.clientY);
	};
	// recalculate grid on resize
	window.addEventListener('mousemove', handleMouseMove);
	// cleanup
	return () => {
		window.removeEventListener('mousemove', handleMouseMove);
	};
}, []);

// ...

<Cell key={i} mouseX={mouseX} mouseY={mouseY} />
```

We have new state here that will store the coordinates of the mouse cursor as _motion_ values. This means we can pass the values to Framer Motion to handle animation.

We've added an event listener to the window object that updates the motion value using an `animate` function whenever the mouse moves. We pass that value down to every cell as a prop.

Now, on to the `Cell` component:

```tsx
import { motion, MotionValue, useTransform } from 'framer-motion';
import { useState, useRef } from 'react';

// ...

interface CellProps {
  mouseX: MotionValue<number>;
  mouseY: MotionValue<number>;
}

const Cell: React.FC<CellProps> = ({ mouseX, mouseY }) => {
  const [position, setPosition] = useState([0, 0]);
  const ref = useRef<HTMLDivElement>(null);

	return <Container ref={ref}>→</Container>;
};

// ...
```

We are receiving the mouse coordinate props here and typing them as motion values that hold a number.

We want to determine the _position_ of each cell. To do that, we've added state to hold coordinates, and a React `ref` we can use to reference the DOM element of the cell.

Let's add a `useEffect` that will set the centre position of the cell to state:

```tsx
import { useState, useRef, useEffect } from 'react';

// ...
  
useEffect(() => {
	if (!ref.current) return;
	const rect = ref.current.getBoundingClientRect();
	// center x coordinate
	const x = rect.left + CELL_SIZE / 2;
	// center y coordinate
	const y = rect.top + CELL_SIZE / 2;
	setPosition([x, y]);
}, [ref.current]);

// ...
```

Every cell now knows it's own coordinates, and is being passed the coordinates of the mouse cursor.

We need to determine the angle of the line from the current cell to the mouse cursor:

```tsx
// ...

const direction = useTransform<number, number>(
	[mouseX, mouseY],
	([newX, newY]) => {
		const diffY = newY - position[1];
		const diffX = newX - position[0];
		const angleRadians = Math.atan2(diffY, diffX);
		const angleDegrees = Math.floor(angleRadians * (180 / Math.PI));
		return angleDegrees;
	}
);

// ...

return (
  <Container ref={ref}>
    <motion.div style={{ rotate: direction }}>→</motion.div>
  </Container>
);

// ...
```

We do this with the `useTransform` hook from Framer Motion. This takes in motion values, transforms them, and gives back a new motion value.

The crucial part here is the `Math.atan2` method that we use to find the angle between the centre of the cell and the mouse cursor. We convert that from radians to degrees so we can pass it directly to our arrow.

The arrow is wrapped in a `motion.div` and we pass it our new motion value to animate.

The arrows should now be following the mouse cursor!

![Moving arrows](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ip45u5e68x5q0jprxu3g.gif)

## Spotlight

We already have a pretty cool effect, but the next touches will really make it impressive.

First off, we'll add the following styles to our `Grid`'s  styled `Container`:

```css
mask-image: radial-gradient(
	300px 300px,
	rgba(0, 0, 0, 1),
	rgba(0, 0, 0, 0.4),
	transparent
);
mask-repeat: no-repeat;
```

We should now see that the middle of our arrow grid is _lit up_ and the outside is darkened. We want this centre mask to follow our mouse.

Add the following underneath our mouse coordinate motion values in the `Grid` component:

```tsx
import {
  animate,
  motion,
  useMotionTemplate,
  useMotionValue,
  useTransform,
} from 'framer-motion';

// ...

const centerMouseX = useTransform<number, number>(mouseX, (newX) => {
	return newX - window.innerWidth / 2;
});
const centerMouseY = useTransform<number, number>(mouseY, (newY) => {
	return newY - window.innerHeight / 2;
});
const WebkitMaskPosition = useMotionTemplate`${centerMouseX}px ${centerMouseY}px`;

// ...
```

By default, our mask will be at the centre of the screen and its placement will be anchored from there. We transform the current mouse position so that the coordinates are from the centre of the screen.

We then create a motion template value with a hook from Framer Motion, and we can add that to our `Grid`'s `Container` component like so:

```tsx
// ...

<Container columns={columns} style={{ WebkitMaskPosition }}>

// ...
```

We should now have a spotlight that follows our mouse cursor!

![Spotlight arrows](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5f9x4set4s13lrsfsbma.gif)

## Velocity Fade

For this final touch we'll need to do some gymnastics to make it smooth, so be warned.

Under our previous mouse motion values in the `Grid` component, add the following:

```tsx
import {
  animate,
  motion,
  useMotionTemplate,
  useMotionValue,
  useTransform,
  useVelocity,
} from 'framer-motion';

// ...

// eased mouse position
const mouseXEased = useMotionValue(0);
const mouseYEased = useMotionValue(0);
// mouse velocity
const mouseXVelocity = useVelocity(mouseXEased);
const mouseYVelocity = useVelocity(mouseYEased);
const mouseVelocity = useTransform<number, number>(
	[mouseXVelocity, mouseYVelocity],
	([latestX, latestY]) => Math.abs(latestX) + Math.abs(latestY)
);
// map mouse velocity to an opacity value
const opacity = useTransform(mouseVelocity, [0, 1000], [0, 1]);

// ...
```

We create a motion value that again holds the mouse coordinates, except this time they are going to be eased. This is what will give us the fading out effect in the end.

We determine the velocity of the mouse cursor by combining the velocity of our x and y coordinates with Framer Motion's `useVelocity` hook.

From there, we map the mouse velocity (I found a range of `0` to `1000` worked well) to an opacity value between `0` and `1`.

We're almost there. We just need to animate the eased mouse coordinates when the mouse moves. Our `handleMouseMove` function inside of `Grid` should look like the following:

```tsx
import {
  animate,
  AnimationOptions,
  motion,
  useMotionTemplate,
  useMotionValue,
  useTransform,
  useVelocity,
} from 'framer-motion';

// ...

const handleMouseMove = (e: MouseEvent) => {
	// animate mouse x and y
	animate(mouseX, e.clientX);
	animate(mouseY, e.clientY);
	// animate eased mouse x and y
	const transition: AnimationOptions<number> = {
		ease: 'easeOut',
		duration: 1,
	};
	animate(mouseXEased, e.clientX, transition);
	animate(mouseYEased, e.clientY, transition);
};

// ...
```

Now we pass the opacity value to the `style` prop of our `Grid`'s `Container`:

```tsx
// ...

<Container
	columns={columns}
	style={{
		opacity,
		WebkitMaskPosition,
	}}
>

// ...
```

The grid should now fade in and out based on the user's mouse velocity. This part really brings the animation together.

![Velocity faded arrows](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yavr0xmmg2ag14p5j79m.gif)

---

Well done for making it this far!

There are techniques here that you can get creative with. It was a learning experience for me, and I'm looking forward to diving deeper.

You can check out the [final project repo](https://github.com/arielbk/pointer-animation) to see the final code, and again here's a link to the [demo](https://pointer-animation.netlify.app/).

Did you have any problems along the way? Are there parts you think could be improved? Did you manage to use the techniques to create something else?

I'd love to hear about it in the comments!