Tutorials

avatar

Malik Kotb

Feb 11, 2024 / Beginner / Short

Cursor Blend Effect
Build a Cursor Blend Effect with Framer Motion and Next.js

A custom cursor with a cursor blend hover effect revealing text underneath. Made with Nextjs (TypeScript) and Framer Motion. Insipired by: https://minhpham.design/

Initializing the project

Let's start the project by setting up a Next.js app with TypeScript and utilizing the app router. Just run npx create-next-app@latest in the terminal.

We can clear out all the contents in the page.tsx, and global.css files, and add our own HTML and CSS, to begin with a clean slate in the application.

Page Component

Our page component is just a simple div that contains our CursorBlendcomopnent..

page.tsx

1import CursorBlend from "./components/CursorBlend";
2export default function page()  {
3	return (
4		<div>
5			<CursorBlend />
6		</div>
7	);
8}
9

We also need a SVG file to use as our mask. You can create a simple 40x40 circle in Figma or download one. Put that SVG file in your /public folder.

Create a styles.module.cssfile in the components folder and add following to it:

page.tsx

1.mask {
2	mask-image: url("../../public/mask.svg");
3}
4

CursorBlend Component

Create a components directory (next to the app directory) and create a new file CursorBlend.tsx.

Imports

page.tsx

1"use client";
2import styles from "../style.module.css";
3import { useEffect, useState } from "react";
4import { motion } from "framer-motion";
5

Framer-Motion only works in client components, therefore we add "use client" at the top of the page. We need to import motion from framer-motion.

Getting Mouse Position

We need to keep track of the current mouse position to know when and where to apply our animation. We also need to know if we are currently hovering our text area or not.

page.tsx

1const [isHovering, setIsHovering] = useState(false);  // hover state
2const cursorSize = isHovering ? 400 : 40;
3const [mousePosition, setMousePosition] = useState({ x: null, y: null });
4
5const manageMouseMove = (e: { clientX: any; clientY: any }) => {
6	const { clientX, clientY } = e;
7	setMousePosition({ x: clientX, y: clientY });
8};
9
10useEffect(() => {
11	window.addEventListener("mousemove",  manageMouseMove);
12	return () => {
13		window.removeEventListener("mousemove",  manageMouseMove);
14	};
15}, []);
16

Animation

page.tsx

1return (
2	<main className="section h-screen bg-black">
3		<motion.div
4			animate={{ WebkitMaskPosition: `${(mousePosition.x || 0) - cursorSize / 2}px
5					${(mousePosition.y || 0) - cursorSize / 2}px`,
6					WebkitMaskSize:  `${cursorSize}px`,
7			}}
8			transition={{ type: "tween", ease: "backOut", duration: 0.5 }}
9			style={{ maskRepeat: "no-repeat", maskSize: "40px", position: "absolute",
10					color: "black", backgroundColor: "#433bff"}}
11			className={`${styles.mask} w-full h-full flex items-center justify-center`}>
12				<p className="w-[80vw] p-10"
13					onMouseEnter={() => { setIsHovering(true) }}
14					onMouseLeave={() => { setIsHovering(false) }}>
15						This is the second text.
16				</p>
17		</motion.div>
18
19		<div className="w-full h-full flex items-center justify-center">
20			<p className="w-[80vw] p-10">
21				This is the first text.
22			</p>
23		</div>
24
25	</main>
26);
27
We should have something like this:
Wrapping up.

Quick and smooth, right?

Hope you liked the animation and learned something new!

-Malik

Things for creative devs
sent to your inbox every week