SVG Mask Effect | Content Inversion Mask
SVG Mask Effect uses pointer position across identical base and mask layers so the default content keeps its original colors while hover reveals an inverted color area.
Effect Showcase
Design Notes
Scenario
Technical content often needs to show the same message in two visual states. The base layer stays readable, while the mask layer provides contrast or emphasis. If the two layers contain different words, the interaction feels like hidden content. If hover expands into a large circular window, the shape becomes louder than the content.
This version keeps the base layer and mask layer identical. The normal view stays in its original colors, and hover reveals the inverted layer only inside the pointer area.
Interaction Design
The point is not a large circular animation. It is aligned content with a localized inversion state:
- Base layer
The default layer owns the readable semantic content.
- Mask layer
The mask layer reuses the same content and only changes the visual treatment.
- Keyboard fallback
Focusing the container triggers a centered inversion state, so the interaction does not depend only on a mouse.
Implementation Model
The Aceternity reference uses motion to drive mask changes. This Lab version keeps the mechanism and removes the runtime dependency: React stores pointer coordinates inside the container and writes them into a radial-gradient() CSS mask.
The component has two absolute layers that render the same content component. The inverted layer is transparent by default. On hover or focus, it becomes visible only inside the radial mask, so the pointer area flips color while the rest of the card keeps the base layer. Tailwind handles layout, borders, and theme tokens; the mask itself is inline style.
SEO and Content Positioning
Useful search intent includes "CSS mask", "hover color inversion", and "same content layers". For this site, the pattern can become theme contrast, state comparison, content emphasis, or AI output versus human correction comparison.
Testing Notes
Test content parity, coordinates, and accessibility: the base and mask text must stay identical, the inverted area follows pointer movement, pointer leave restores the default layer, keyboard focus triggers the fallback inversion state, and the mask layer must not block semantics or pointer events.
Copyable Source
Source is read from the case examples folder, so the displayed snippet and typechecked code stay aligned.
"use client"; import * as React from "react"; type MaskState = { x: number; y: number; active: boolean;}; function pointerPosition(event: React.PointerEvent<HTMLElement>) { const rect = event.currentTarget.getBoundingClientRect(); return { x: event.clientX - rect.left, y: event.clientY - rect.top, };} const contentPoints = ["Same content", "Hover inversion", "No zoom bubble"]; function MaskContent({ variant }: { variant: "base" | "mask" }) { const isMask = variant === "mask"; const eyebrowClassName = isMask ? "text-background/70" : "text-muted-foreground"; const titleClassName = isMask ? "text-background" : "text-foreground"; const bodyClassName = isMask ? "text-background/75" : "text-muted-foreground"; const pointClassName = isMask ? "border-background/20 bg-background/10 text-background/80" : "border-border bg-muted/40 text-muted-foreground"; return ( <div className="max-w-md text-center"> <p className={`text-xs font-medium uppercase tracking-[0.18em] ${eyebrowClassName}`} > Inversion mask </p> <h3 className={`mt-3 text-2xl font-semibold leading-tight ${titleClassName}`} > One content stack rendered twice, aligned for an inverted hover highlight. </h3> <p className={`mt-3 text-sm leading-6 ${bodyClassName}`}> The mask layer keeps the same words as the base layer. Hover only flips the color under the pointer instead of tinting the whole panel. </p> <div className="mt-5 grid gap-2 sm:grid-cols-3"> {contentPoints.map((point) => ( <span className={`rounded-md border px-3 py-2 text-xs font-medium ${pointClassName}`} key={point} > {point} </span> ))} </div> </div> );} export function SvgMaskReveal() { const [mask, setMask] = React.useState<MaskState>({ x: 240, y: 150, active: false, }); const highlightRadius = 44; const maskImage = mask.active ? `radial-gradient(circle ${highlightRadius}px at ${mask.x}px ${mask.y}px, black 0%, black 56%, transparent 76%)` : "linear-gradient(transparent, transparent)"; const maskStyle: React.CSSProperties = { WebkitMaskImage: maskImage, maskImage, }; return ( <div className="rounded-card border border-border bg-background p-2 shadow-soft"> <div className="relative min-h-[360px] overflow-hidden rounded-card border border-border bg-card outline-none focus-visible:ring-2 focus-visible:ring-ring" aria-label="SVG mask color inversion demo" onBlur={() => setMask((current) => ({ ...current, active: false }))} onFocus={() => setMask({ x: 240, y: 170, active: true })} onPointerEnter={(event) => setMask({ ...pointerPosition(event), active: true }) } onPointerLeave={() => setMask((current) => ({ ...current, active: false })) } onPointerMove={(event) => setMask({ ...pointerPosition(event), active: true }) } role="group" tabIndex={0} > <div className="absolute inset-0 flex items-center justify-center p-6" data-svg-mask-base > <MaskContent variant="base" /> </div> <div aria-hidden="true" className={`pointer-events-none absolute inset-0 flex items-center justify-center bg-foreground p-6 text-background transition-opacity duration-200 ${ mask.active ? "opacity-100" : "opacity-0" }`} data-svg-mask-layer style={maskStyle} > <MaskContent variant="mask" /> </div> </div> </div> );} export default SvgMaskReveal;Agent Prompt
Agent Prompt
Build an SVG Mask Effect interaction inspired by https://ui.aceternity.com/components/svg-mask-effect, but simplify it for this Lab: use React, pointer events, CSS `mask-image` / `-webkit-mask-image`, inline radial gradients, and existing Tailwind utility classes only. Do not install motion libraries, do not add a mask asset, and do not change Tailwind config. Deliver one self-contained React component, a small preview, and localized Markdown content. The base layer and mask layer should render identical content. The default state should keep the original case colors, while hover should reveal the inverted mask layer only under the pointer instead of flipping the whole panel. Add a focus fallback so keyboard users can trigger the same inverted highlight without a pointer. Acceptance: base and mask text stay identical, hover/focus switches the mask to an inverted pointer area, pointer leave restores the calm base state, the mask layer does not block pointer or content semantics, and the example source is the same component used by the demo.