DEV Community

Cover image for Mask Cursor Effect
Cass
Cass

Posted on

Mask Cursor Effect

A simple cursor effect that follows your mouse.
If you only came for the code here is the repo.
Inspired by Minh Pham portfolio.

Initializing the project

Create a simple Nextjs project with TailwindCSS

npx create-next-app
Enter fullscreen mode Exit fullscreen mode

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

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

Now, our mouse position hook, if you don't know about hooks then check this.
The resulting code of our mousePosition.js

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
Enter fullscreen mode Exit fullscreen mode

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

'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>
  )
}
Enter fullscreen mode Exit fullscreen mode

And our mask.module.css

.mask {
  mask-image: url('../../public/mask.svg');
  mask-repeat: no-repeat;
  mask-size: 40px;
  position: absolute;
}
Enter fullscreen mode Exit fullscreen mode

And that's the final result.
You can see the demo here
Final Result GIF

Top comments (0)