Mask Cursor Effect
Tags: tailwindcss, react, nextjs, css
Published: 2023-11-10 23:47:36

A simple cursor effect that follows your mouse.
If you only came for the code [here is the repo](https://github.com/7cass/cursor-mask-effect).
Inspired by [Minh Pham](https://minhpham.design/) portfolio.

## Initializing the project
Create a simple Nextjs project with TailwindCSS

```bash
npx create-next-app
```
Then we can create two elements, each one with the same height and one of our elements must have an absolute position so we can make them overlap each other. I have added some style just to make it fancy, but heres the resulting code of our `page.tsx`

```tsx
export default function Home() {
  return (
    <main className="h-screen cursor-default">

      <div className="w-full h-full flex items-center justify-center text-black bg-red-500">
        <h1 className="text-9xl font-light">I&apos;m a Software Developer</h1>
      </div>

      <div className="w-full h-full flex items-center justify-center bg-black text-red-500">
        <h1 className="text-9xl font-light">I don&apos;t know what am I doing</h1>
      </div>

    </main>
  )
}
```
Now, our mouse position hook, if you don't know about hooks then check [this](https://react.dev/learn/reusing-logic-with-custom-hooks).
The resulting code of our `mousePosition.js`
```ts
import { useState, useEffect } from 'react'

type Position = {
  x: number | null
  y: number | null
}

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

  const updateMousePosition = (e: MouseEvent) => {
    setMousePosition({
      x: e.clientX + window.scrollX,
      y: e.clientY + window.scrollY,
    })
  }

  useEffect(() => {
    window.addEventListener('mousemove', updateMousePosition)

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

  useEffect(() => {
    window.addEventListener('mousemove', updateMousePosition)
  }, [mousePosition])

  return mousePosition
}

export default useMousePosition
```
The mouse position is tracked every time the `mousemove` event is triggered by our `useEffect()`, and we store it into our `useState()`. 
We also calculate the window scroll to be used with a scrollable element.

Moving foward let's get back to our `page.tsx` and continue... But before, we need Framer Motion to handle the animations.

`npm i framer-motion`

Now, we need to use the directive `'use client'`, because our component is now a client component, so let's place it at the beginning of our file. Let's implement Framer Motion along with our hook and add some css because Tailwind doesn't have a good support for masks yet.
Our `page.tsx`

```tsx
'use client';
import useMousePosition from "@/hooks/useMousePosition"
import { motion } from 'framer-motion';
import { useState } from "react";

import styles from './mask.module.css'

export default function Home() {
  const [isHovered, setIsHovered] = useState(false)
  const [maskScale, setMaskScale] = useState(50)
  const { x, y } = useMousePosition()
  const size = isHovered ? maskScale * 10 : maskScale

  const handleMouseToggle = (scale: number, hover: boolean): void => {
    setMaskScale(scale)
    setIsHovered(hover)
  }

  return (
    <main className="h-screen cursor-default">

      <motion.div
        animate={{
          WebkitMaskPosition: `${Number(x) - size / 2}px ${
            Number(y) - size / 2
          }px`,
          WebkitMaskSize: `${size}px`,
        }}
        transition={{ type: 'tween', ease: 'backOut', duration: 0.5 }}
        className={`${styles.mask} absolute w-full h-full flex items-center justify-center text-black bg-red-500`}>
        <h1 
          onMouseEnter={() => handleMouseToggle(50, true)}
          onMouseLeave={() => handleMouseToggle(50, false)}
          className="text-9xl font-light">I&apos;m a Software Developer</h1>
      </motion.div>

      <div className="w-full h-full flex items-center justify-center bg-black text-red-500">
        <h1 className="text-9xl font-light">I don&apos;t know what am I doing</h1>
      </div>

    </main>
  )
}
``` 
And our `mask.module.css`
```css
.mask {
  mask-image: url('../../public/mask.svg');
  mask-repeat: no-repeat;
  mask-size: 40px;
  position: absolute;
}
```
And that's the final result.
You can see the demo [here](https://cursor-mask-effect.vercel.app) 
![Final Result GIF](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ia9niqd0pzzf20lk4zgz.gif)