How to build a cursor-reveal hero section in React
A cursor-reveal hero in React stacks two layers and clips the top one to a circular clip-path that follows the pointer via a Framer Motion spring, revealing the layer beneath. Build it with useMotionValue for the position and useSpring for the radius.
- Stack: React + Framer Motion + Tailwind v4, ~290 lines, zero extra dependencies.
- Core API: useMotionValue, useSpring, useMotionTemplate, clip-path.
- Accessible: both layers carry the same readable text; the mask is decorative.
- Needs a static fallback on touch devices (no pointer).
Hero Cursor Mask is a full-screen React hero where moving the cursor reveals a second, lighter layer through a spring-animated circular window. It turns a static headline into a tactile, explore-me surface, the kind of detail that makes a landing page feel handcrafted rather than templated.
Anatomy
Two identical content layers are stacked absolutely: a dark base layer (background + dot grid + radial vignette) and a light layer on top. The light layer is hidden everywhere except inside a circular clip-path that tracks the pointer. A 12px accent dot acts as a custom cursor, blended with mix-blend-difference so it stays visible over both layers.
How it works
The effect rides on Framer Motion's useMotionValue + useSpring. Pointer coordinates feed two motion values (mouseX/mouseY); the circle radius is its own spring (stiffness 200, damping 28) that expands from 0 to 180px on enter and collapses on leave. useMotionTemplate stitches them into a live `circle(${size}px at ${x}px ${y}px)` clip-path string, so the reveal follows the cursor with natural inertia instead of snapping.
How to build it in React
Stack two full-screen layers
Render the same content twice inside a relative container with overflow:hidden. The bottom layer uses your dark theme, the top layer the light one. Set the container cursor to none, you'll draw your own.
Track the pointer with motion values
On mousemove, convert clientX/Y to container-relative coordinates with getBoundingClientRect and write them to mouseX/mouseY motion values. Keep a separate spring for the radius so it eases in and out.
const mouseX = useMotionValue(0); const size = useSpring(rawSize, { stiffness: 200, damping: 28 }); const clipPath = useMotionTemplate`circle(${size}px at ${mouseX}px ${mouseY}px)`;Clip the top layer
Apply the motion clipPath (and -webkit-clip-path) to the light layer. Add a small motion.div positioned at mouseX/mouseY with mixBlendMode:'difference' as the custom cursor.
When to use it
Reach for it on a brand/product landing hero where you want one memorable interaction above the fold, agencies, design tools, AI products. Avoid it on content-dense or conversion-critical pages where a clear CTA matters more than delight, and provide a static fallback for touch devices (there is no cursor to follow).
Used by
FAQ
Does the cursor mask work on mobile?
No, there is no pointer on touch screens, so ship a static version of the light or dark layer as the mobile fallback and skip the mousemove listeners.
Why use a spring instead of a CSS transition?
The spring gives the reveal weight and inertia so it trails the cursor naturally; a linear CSS transition feels mechanical and lags uniformly regardless of speed.
Is it accessible?
Both layers contain the same readable text, so screen readers and no-JS users still get the full content; the mask is purely decorative and the dark layer stands on its own.