Text Reveal Card

Mousemove effect to reveal text content at the bottom of the card.

Sometimes, you just need to see it.

This is a text reveal card. Hover over the card to reveal the hidden text.

I know the chemistry

You know the business

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));
}

Copy the source code

src/lib/components/ui/TextRevealCard/Stars.svelte
<script lang="ts">
	import { Motion } from 'svelte-motion';
	let randomMove = () => Math.random() * 4 - 2;
</script>

<div class="absolute inset-0 z-40">
	{#each [...Array(140)] as _, i (`star-${i}`)}
		<Motion
			let:motion
			animate={{
				top: `calc(${Math.random() * 100}% + ${randomMove()}px)`,
				left: `calc(${Math.random() * 100}% + ${randomMove()}px)`,
				opacity: Math.random(),
				scale: [1, 1.2, 0]
			}}
			transition={{
				duration: Math.random() * 10 + 20,
				repeat: Infinity,
				ease: 'linear'
			}}
		>
			<span
				use:motion
				class="z-50 inline-block"
				style={`position: absolute; width: 2px; height: 2px; background-color: white; border-radius: 50%; top: ${Math.random() * 100}%; left: ${Math.random() * 100}%; z-index: 1;`}
			></span>
		</Motion>
	{/each}
</div>
src/lib/components/ui/TextRevealCard/TextRevealCard.svelte
<script lang="ts">
	import { Motion } from 'svelte-motion';
	import { cn } from '$lib/utils';
	import { onMount } from 'svelte';
	import Stars from './Stars.svelte';

	export let text: string;
	export let revealText: string;
	export let className: string | undefined = undefined;

	let widthPercentage = 0;
	let cardRef: HTMLDivElement;
	let left = 0;
	let localWidth = 0;
	let isMouseOver = false;

	onMount(() => {
		if (cardRef) {
			const { left: newLeft, width: newLocalWidth } = cardRef.getBoundingClientRect();
			left = newLeft;
			localWidth = newLocalWidth;
		}
	});

	function mouseMoveHandler(event: any) {
		event.preventDefault();

		const { clientX } = event;
		if (cardRef) {
			const relativeX = clientX - left;
			widthPercentage = (relativeX / localWidth) * 100;
		}
	}

	function mouseLeaveHandler() {
		isMouseOver = false;
		widthPercentage = 0;
	}
	function mouseEnterHandler() {
		isMouseOver = true;
	}

	const rotateDeg = (widthPercentage - 50) * 0.1;
</script>

<div
	on:mouseenter={mouseEnterHandler}
	on:mouseleave={mouseLeaveHandler}
	on:mousemove={mouseMoveHandler}
	bind:this={cardRef}
	class={cn(
		'relative w-[40rem] overflow-hidden rounded-lg border border-white/[0.08] bg-[#1d1c20] p-8',
		className
	)}
>
	<slot />

	<div class="relative flex h-40 items-center overflow-hidden">
		<Motion
			let:motion
			style={{
				width: '100%'
			}}
			animate={isMouseOver
				? {
						opacity: widthPercentage > 0 ? 1 : 0,
						clipPath: `inset(0 ${100 - widthPercentage}% 0 0)`
					}
				: {
						clipPath: `inset(0 ${100 - widthPercentage}% 0 0)`
					}}
			transition={isMouseOver ? { duration: 0 } : { duration: 0.4 }}
		>
			<div use:motion class="absolute z-20 bg-[#1d1c20] will-change-transform">
				<p
					style="text-shadow: 4px 4px 15px rgba(0,0,0,0.5);"
					class="bg-gradient-to-b from-white to-neutral-300 bg-clip-text py-10 text-base font-bold text-transparent text-white sm:text-[3rem]"
				>
					{revealText}
				</p>
			</div>
		</Motion>
		<Motion
			let:motion
			animate={{
				left: `${widthPercentage}%`,
				rotate: `${rotateDeg}deg`,
				opacity: widthPercentage > 0 ? 1 : 0
			}}
			transition={isMouseOver ? { duration: 0 } : { duration: 0.4 }}
		>
			<div
				use:motion
				class="absolute z-50 h-40 w-[8px] bg-gradient-to-b from-transparent via-neutral-800 to-transparent will-change-transform"
			></div>
		</Motion>

		<div
			class=" overflow-hidden [mask-image:linear-gradient(to_bottom,transparent,white,transparent)]"
		>
			<p
				class="bg-[#323238] bg-clip-text py-10 text-base font-bold text-transparent sm:text-[3rem]"
			>
				{text}
			</p>
			<Stars />
		</div>
	</div>
</div>
src/lib/components/ui/TextRevealCard/TextRevealCardDescription.svelte
<script lang="ts">
	import { twMerge } from 'tailwind-merge';

	export let className: string | undefined = undefined;
</script>

<p class={twMerge('text-sm text-[#a9a9a9]', className)}><slot /></p>
src/lib/components/ui/TextRevealCard/TextRevealCardTitle.svelte
<script lang="ts">
	import { twMerge } from 'tailwind-merge';

	export let className: string | undefined = undefined;
</script>

<h2 class={twMerge('mb-2 text-lg text-white', className)}>
	<slot />
</h2>
src/lib/components/ui/TextRevealCard/index.ts
import TextRevealCard from './TextRevealCard.svelte';
import TextRevealCardTitle from './TextRevealCardTitle.svelte';
import TextRevealCardDescription from './TextRevealCardDescription.svelte';
import Stars from './Stars.svelte';

export { TextRevealCard, TextRevealCardTitle, TextRevealCardDescription, Stars };

Props

TextRevealCard
Prop Type Description
text string Piece of text that stays on the card
revealText string Text that reveals when hovered over the card
className string | undefined The class name of the TextReveal component.
TextRevealCardTitle
Prop Type Description
className string | undefined CSS class name for the title.
TextRevealCardDescription
Prop Type Description
className string | undefined CSS class name for the description.