Tutorials
Malik Kotb
Feb 11, 2024 / Beginner / Short
A custom cursor with a cursor blend hover effect revealing text underneath. Made with Nextjs (TypeScript) and Framer Motion. Insipired by: https://minhpham.design/
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.
npm i framer-motion
.Our page component is just a simple div that contains our CursorBlend
comopnent..
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.css
file in the components folder and add following to it:
page.tsx
1.mask {
2 mask-image: url("../../public/mask.svg");
3}
4
Create a components directory (next to the app directory) and create a new file CursorBlend.tsx
.
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
.
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
manageMouseMove
, where we update the current mouse position state.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
WebkitMaskPosition
and WebkitMaskSize
control the position and size of our mask element based on our mouse's current position. We calculate the position relative to the mouse coordinates and adjust the size according to the cursorSize
variable.