SVG Mask Effect | 内容反色遮罩
SVG Mask Effect 用指针位置驱动同内容双层遮罩,默认保持案例原色,只在 hover 位置显示反色区域,适合作为技术博客中的状态对照案例。
效果展示
设计说明
场景
技术内容经常需要展示同一段内容在两种视觉状态下的差异:底层保持正常阅读,蒙层提供反差或强调。如果两层文字不一致,读者会以为是在揭示另一套信息;如果 hover 又放大成一个圆形窗口,视觉焦点会偏离内容本身。
这个版本把底层和蒙层保持为同一份内容。默认视图保持原本颜色,hover 时只在指针位置露出反色蒙层。效果重点从“整体反色”转为“局部反色”。
交互设计
这个案例的重点不是放大的圆形动效,而是内容对齐后的局部反色关系:
- Base layer
默认层负责语义和可读性,用户没有 hover 时看到的是稳定内容。
- Mask layer
蒙层复用同一份内容,只改变色彩和背景,避免两层信息不一致。
- Keyboard fallback
容器获得焦点时触发居中的反色状态,避免只服务鼠标用户。
实现原理
Aceternity 的原案例使用 motion 来驱动 mask 变化。本 Lab 版本只保留机制:React 记录指针在容器内的局部坐标,并把坐标写入 radial-gradient() 形式的 CSS mask。
实现上有两层绝对定位内容,底层和蒙层渲染同一个内容组件。反色蒙层默认透明;hover 或 focus 时,蒙层只在径向遮罩范围内显示,让指针附近区域变成反色,其余区域继续显示底层原色。Tailwind 负责布局、边框和色彩 token,遮罩本身用内联样式完成。
SEO 与内容定位
这个案例适合围绕 “CSS mask”、“hover color inversion”、“same content layers” 做搜索语义。对个人技术博客来说,它可以扩展成主题反差、状态对照、内容强调、AI 生成结果与人工校正对比等内容。
测试说明
测试重点是内容一致性、坐标和可访问性:底层与蒙层的文本必须一致;移动指针时反色区域跟随指针;离开容器后恢复默认层;键盘 focus 后能触发居中的反色状态;蒙层不能阻塞底层语义或指针事件。
可复制源码
源码从案例目录的 examples 文件读取,展示内容和参与 typecheck 的代码保持同一事实来源。
"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.