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 framer-motion clsx tailwind-merge
Add util file
lib/utils.ts
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Copy the source code
components/ui/text-reveal-card.tsx
"use client";
import React, { useEffect, useRef, useState, memo } from "react";
import { motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import { cn } from "@/lib/utils";
export const TextRevealCard = ({
text,
revealText,
children,
className,
}: {
text: string;
revealText: string;
children?: React.ReactNode;
className?: string;
}) => {
const [widthPercentage, setWidthPercentage] = useState(0);
const cardRef = useRef<HTMLDivElement | any>(null);
const [left, setLeft] = useState(0);
const [localWidth, setLocalWidth] = useState(0);
const [isMouseOver, setIsMouseOver] = useState(false);
useEffect(() => {
if (cardRef.current) {
const { left, width: localWidth } =
cardRef.current.getBoundingClientRect();
setLeft(left);
setLocalWidth(localWidth);
}
}, []);
function mouseMoveHandler(event: any) {
event.preventDefault();
const { clientX } = event;
if (cardRef.current) {
const relativeX = clientX - left;
setWidthPercentage((relativeX / localWidth) * 100);
}
}
function mouseLeaveHandler() {
setIsMouseOver(false);
setWidthPercentage(0);
}
function mouseEnterHandler() {
setIsMouseOver(true);
}
function touchMoveHandler(event: React.TouchEvent<HTMLDivElement>) {
event.preventDefault();
const clientX = event.touches[0]!.clientX;
if (cardRef.current) {
const relativeX = clientX - left;
setWidthPercentage((relativeX / localWidth) * 100);
}
}
const rotateDeg = (widthPercentage - 50) * 0.1;
return (
<div
onMouseEnter={mouseEnterHandler}
onMouseLeave={mouseLeaveHandler}
onMouseMove={mouseMoveHandler}
onTouchStart={mouseEnterHandler}
onTouchEnd={mouseLeaveHandler}
onTouchMove={touchMoveHandler}
ref={cardRef}
className={cn(
"bg-[#1d1c20] border border-white/[0.08] w-[40rem] rounded-lg p-8 relative overflow-hidden",
className
)}
>
{children}
<div className="h-40 relative flex items-center overflow-hidden">
<motion.div
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 }}
className="absolute bg-[#1d1c20] z-20 will-change-transform"
>
<p
style={{
textShadow: "4px 4px 15px rgba(0,0,0,0.5)",
}}
className="text-base sm:text-[3rem] py-10 font-bold text-white bg-clip-text text-transparent bg-gradient-to-b from-white to-neutral-300"
>
{revealText}
</p>
</motion.div>
<motion.div
animate={{
left: `${widthPercentage}%`,
rotate: `${rotateDeg}deg`,
opacity: widthPercentage > 0 ? 1 : 0,
}}
transition={isMouseOver ? { duration: 0 } : { duration: 0.4 }}
className="h-40 w-[8px] bg-gradient-to-b from-transparent via-neutral-800 to-transparent absolute z-50 will-change-transform"
></motion.div>
<div className=" overflow-hidden [mask-image:linear-gradient(to_bottom,transparent,white,transparent)]">
<p className="text-base sm:text-[3rem] py-10 font-bold bg-clip-text text-transparent bg-[#323238]">
{text}
</p>
<MemoizedStars />
</div>
</div>
</div>
);
};
export const TextRevealCardTitle = ({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) => {
return (
<h2 className={twMerge("text-white text-lg mb-2", className)}>
{children}
</h2>
);
};
export const TextRevealCardDescription = ({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) => {
return (
<p className={twMerge("text-[#a9a9a9] text-sm", className)}>{children}</p>
);
};
const Stars = () => {
const randomMove = () => Math.random() * 4 - 2;
const randomOpacity = () => Math.random();
const random = () => Math.random();
return (
<div className="absolute inset-0">
{[...Array(80)].map((_, i) => (
<motion.span
key={`star-${i}`}
animate={{
top: `calc(${random() * 100}% + ${randomMove()}px)`,
left: `calc(${random() * 100}% + ${randomMove()}px)`,
opacity: randomOpacity(),
scale: [1, 1.2, 0],
}}
transition={{
duration: random() * 10 + 20,
repeat: Infinity,
ease: "linear",
}}
style={{
position: "absolute",
top: `${random() * 100}%`,
left: `${random() * 100}%`,
width: `2px`,
height: `2px`,
backgroundColor: "white",
borderRadius: "50%",
zIndex: 1,
}}
className="inline-block"
></motion.span>
))}
</div>
);
};
export const MemoizedStars = memo(Stars);
Props
Prop name | Type | Description |
---|---|---|
className | string | The class name of the TextReveal component. |
text | string | Piece of text that stays on the card |
revealText | string | Text that reveals when hovered over the card |
children | React Node | Title and Descriptions for the Card |
Get beautiful, hand-crafted templates and components with Aceternity UI Pro
Professional, beautiful and elegant templates for your business. Get the best component packs and templates with Aceternity UI Pro.
Manu is an artist, I didn't know what I wanted when we started, but his intuition and eye for design more than made up for it. We went from “I want something dark theme and high...
John Ferry
President at TAC, CEO at Rogue