webdevpuneet.com

Cursor Circle Image Reveal — CSS Mask Hover Snippet

Cursor Circle Image Reveal · Animations · Plain HTML, CSS & JS · Live preview

CategoryAnimations

Copy & paste

Source Code

<div class="cm-stage">
  <p class="cm-hint">Move your cursor across the photo — a circular window reveals the alternate version underneath</p>
  <div class="cm-frame" id="cmFrame">
    <div class="cm-base" aria-hidden="true"></div>
    <div class="cm-mask" id="cmMask" aria-hidden="true"></div>
    <span class="cm-tag cm-tag-day">Day</span>
    <span class="cm-tag cm-tag-night">Night</span>
  </div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Two full-size stacked layers; a CSS mask-image reveals the top one only inside a moving circle
Mask position driven entirely by CSS custom properties — no canvas or per-frame JS repainting
radial-gradient(circle Npx at var(--x) var(--y)) used directly as mask-image
getBoundingClientRect gives frame-relative cursor coordinates, not viewport-relative
mouseleave moves the window fully off-frame to cleanly hide the top layer again
touchmove support mirrors the mouse behavior for mobile drag interaction
Adjustable reveal radius and edge softness via the gradient stops
Export as HTML file, React JSX, or React + Tailwind CSS
Mobile (375px), Tablet (768px), Desktop device preview buttons
Live split-pane editor — preview updates as you type

About this UI Snippet

Cursor Circle Image Reveal — CSS mask-image radial-gradient Following the Cursor

Screenshot of the Cursor Circle Image Reveal snippet rendered live

This effect stacks two full-size image layers and cuts a small circular window through the top layer that follows the cursor, so moving the mouse feels like sliding a flashlight or magnifying glass across the surface, exposing the second layer underneath only where the circle currently sits. It's a different interaction from cursor spotlight reveal, which dims and lights up text on a single layer — here two entirely different visuals (day and night versions of the same scene) exist simultaneously, and the cursor decides which one is visible at each point.

The two-layer stack

.cm-base is the bottom layer and is always fully visible — think of it as the "default" image. .cm-mask sits directly on top at the same size (position: absolute; inset: 0) holding the alternate visual, but with a CSS mask-image applied that hides all of it except a small circle.

The circular mask

The mask is a radial-gradient(circle 90px at var(--cm-x, -200px) var(--cm-y, -200px), #000 98%, transparent 100%) used as mask-image (and -webkit-mask-image for Safari). Wherever the gradient is opaque black, the layer beneath shows through; wherever it fades to transparent, that part of the top layer is invisible, letting the base layer beneath show instead. Because the gradient's center position is driven entirely by two CSS custom properties, moving those properties is all it takes to move the visible window.

Tracking the cursor

A single mousemove listener on the frame computes the cursor's position relative to the frame with getBoundingClientRect(), then calls setSpot(x, y), which writes --cm-x and --cm-y via style.setProperty. Because these are plain CSS custom properties read directly inside the radial-gradient() function, the browser's own compositor updates the mask position — there's no JavaScript recalculating gradients or repainting canvases on every frame, keeping the effect smooth even on modest hardware.

Off-frame reset

On mouseleave, setSpot(-200, -200) moves the circular window's center far outside the visible frame, so the mask closes to fully hide the alternate layer again without any special-case CSS — the same radial-gradient math simply places the circle where nothing is visible.

Touch support

A parallel touchmove listener reads e.touches[0] and applies the same relative-position math, so dragging a finger across the frame on mobile reveals the layer underneath exactly like a mouse would.

Adapting it to real photography

Swap the two gradient-based .cm-base/.cm-mask backgrounds for background-image: url(...) pointing at your own day/night, before/after, or sketch/final image pairs — the masking mechanism itself doesn't care what's underneath, only that both layers are the same size and positioned identically.

Step by step

How to Use

  1. 1
    Move the cursor over the frameDrag your cursor across the image area — a circular window follows it, revealing the night-sky layer underneath the day layer.
  2. 2
    Leave the frameMove the cursor off the frame and the window closes, hiding the alternate layer completely again.
  3. 3
    Swap in real imagesReplace the .cm-base and .cm-mask backgrounds with background-image: url(your-image.jpg) for real before/after photography.
  4. 4
    Resize the reveal windowChange the 90px radius value inside the radial-gradient() in the .cm-mask mask-image rule.
  5. 5
    Soften or harden the edgeAdjust the 98% / 100% gradient stops — closer together gives a crisp circle edge, farther apart gives a soft feathered edge.
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for a React component, or "Tailwind" for a React + Tailwind version.

Real-world uses

Common Use Cases

Before/after photography and product shots
Reveal a retouched, renovated, or restored version of a photo only where the cursor hovers, for a more playful alternative to a before/after comparison slider.
Real estate day/night or virtual staging
Let visitors "flashlight" between a daytime and evening render of a property, or a staged versus empty room, directly on hover.
Learn CSS mask-image with custom properties
A focused example of driving a mask-image radial-gradient purely through CSS custom properties updated from JS — no canvas required.
Portfolio sketch-to-final reveals
Designers and illustrators can show a rough sketch layer underneath a finished piece, revealed by hovering across the artwork.
Map or diagram overlay comparisons
Compare two versions of a map, chart, or diagram (old vs. new data, wireframe vs. final) using the same circular reveal mechanism.
Hero section visual storytelling
Use a large cursor-revealed frame as a hero visual to invite exploration before the user scrolls further.

Got questions?

Frequently Asked Questions

A CSS mask-image on the top layer is a radial-gradient centered at a position controlled by two custom properties (--cm-x, --cm-y). Wherever the gradient is opaque, the top layer shows; wherever it fades to transparent, the layer beneath shows through instead. Moving the custom properties moves the visible circle.

Writing to two custom properties via style.setProperty lets the browser's compositor handle repositioning the existing gradient efficiently. Rebuilding and reassigning the full mask-image string on every mousemove would work too, but is unnecessary string work compared to updating two numeric properties.

Yes. Replace the background (and remove the ::after decorative layers) on .cm-base and .cm-mask with background-image: url(...) pointing at two same-size images — the masking logic is unaffected by what image is underneath.

Change the 90px value inside radial-gradient(circle 90px at ...) in the .cm-mask mask-image (and -webkit-mask-image) rule.

Yes. A touchmove listener reads the first active touch point and applies the same relative-position calculation as the mouse handler, so dragging a finger reveals the layer underneath.

Yes. Click "JSX" for a React component. Track the frame with a ref, compute the relative position inside onMouseMove using getBoundingClientRect, and call ref.current.style.setProperty directly for both custom properties to avoid state-driven re-renders on every move.