SVG Mask Effect

A mask reveal effect, hover the cursor over a container to reveal what is underneath.

The first rule of MRR Club is you do not talk about MRR Club. The second rule of MRR Club is you DO NOT talk about MRR Club.

The first rule of MRR Club is you do not talk about MRR Club. The second rule of MRR Club is you DO NOT talk about MRR Club.

Installation

Install Dependencies

npm i svelte-motion clsx tailwind-merge

Add util file

src/lib/utils/cn.ts
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
        
export function cn(...inputs: ClassValue[]) {
    return twMerge(clsx(inputs));
}

Add mask in static folder

static/mask.svg
<svg
  width="1298"
  height="1298"
  viewBox="0 0 1298 1298"
  fill="none"
  xmlns="http://www.w3.org/2000/svg"
>
  <circle cx="649" cy="649" r="649" fill="black" />
</svg>

Copy the source code

src/lib/components/ui/SvgMaskEffect/SvgMaskEffect.svelte
<script lang="ts">
	import { onMount } from 'svelte';
	import { Motion } from 'svelte-motion';
	import { cn } from '$lib/utils';

	export let size: number | undefined = 10;
	export let revealSize: number | undefined = 600;
	export let className: string | undefined = undefined;

	let isHovered = false;
	let mousePosition: { x: number | null; y: number | null } = { x: null, y: null };
	let containerRef: HTMLDivElement;
	const updateMousePosition = (e: any) => {
		const rect = containerRef.getBoundingClientRect();
		mousePosition = { x: e.clientX - rect.left, y: e.clientY - rect.top };
	};

	onMount(() => {
		containerRef.addEventListener('mousemove', updateMousePosition);
		return () => {
			if (containerRef) {
				containerRef.removeEventListener('mousemove', updateMousePosition);
			}
		};
	});

	$: maskSize = isHovered ? revealSize : size;
</script>

<Motion
	let:motion
	animate={{
		backgroundColor: isHovered ? 'var(--slate-900)' : 'var(--white)'
	}}
>
	<div use:motion bind:this={containerRef} class={cn('relative h-screen', className)}>
		<Motion
			let:motion
			animate={{
				WebkitMaskPosition:
					mousePosition.x &&
					mousePosition.y &&
					maskSize &&
					`${mousePosition?.x - maskSize / 2}px ${mousePosition.y - maskSize / 2}px`,
				WebkitMaskSize: `${maskSize}px`
			}}
			transition={{ type: 'tween', ease: 'backOut', duration: 0.1 }}
		>
			<div
				use:motion
				class="absolute flex h-full w-full items-center justify-center bg-black text-6xl text-white bg-grid-white/[0.2] [mask-image:url(/mask.svg)] [mask-repeat:no-repeat] [mask-size:40px]"
			>
				<div class="absolute inset-0 z-0 h-full w-full bg-black opacity-50" />
				<div
					on:mouseenter={() => {
						isHovered = true;
					}}
					on:mouseleave={() => {
						isHovered = false;
					}}
					class="relative z-20 mx-auto max-w-4xl text-center text-4xl font-bold text-white"
				>
					<slot />
				</div>
			</div>
		</Motion>

		<div class="flex h-full w-full items-center justify-center text-white">
			<slot name="revealText" />
		</div>
	</div>
</Motion>
src/lib/components/ui/SvgMaskEffect/index.ts
import SvgMaskEffect from './SvgMaskEffect.svelte';

export { SvgMaskEffect };

Props

SvgMaskEffect
Prop Type Description
size number | undefined Size of the mask
revealSize number | undefined Hovered over size of the mask
className string | undefined The class name of the child component.