Components
Text Hover
Text Hover
Interactive text effect with layered shadows, particles, and smooth animations on hover.
UI STUDIO
UI STUDIO
UI STUDIO
Installation
1
Install the packages
npm i motion clsx tailwind-merge
2
Add util file
lib/util.ts
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
3
Copy and paste the following code into your project
text-hover.tsx
"use client";
import React, { useState } from "react";
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
export function TextHover() {
const [isHovered, setIsHovered] = useState(false);
const text = "UI STUDIO";
return (
<div className="h-[40rem] flex items-center justify-center bg-slate-50 dark:bg-slate-950">
<div
className="relative cursor-pointer"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{/* Main text */}
<motion.h1
className="text-6xl md:text-8xl lg:text-9xl font-black text-slate-900 dark:text-slate-100 select-none"
animate={{
scale: isHovered ? 1.05 : 1,
}}
transition={{ duration: 0.3, ease: "easeOut" }}
>
{text.split("").map((letter, index) => (
<motion.span
key={index}
className="inline-block"
animate={{
y: isHovered ? Math.sin(index * 0.5) * 10 : 0,
color: isHovered ? "#64748b" : undefined,
}}
transition={{
duration: 0.4,
delay: isHovered ? index * 0.05 : 0,
ease: "easeOut",
}}
>
{letter === " " ? "\u00A0" : letter}
</motion.span>
))}
</motion.h1>
{/* Shadow text */}
<motion.h1
className="absolute inset-0 text-6xl md:text-8xl lg:text-9xl font-black text-slate-400 dark:text-slate-600 select-none -z-10"
animate={{
x: isHovered ? 8 : 0,
y: isHovered ? 8 : 0,
opacity: isHovered ? 0.5 : 0,
}}
transition={{ duration: 0.3, ease: "easeOut" }}
>
{text}
</motion.h1>
{/* Outline text */}
<motion.h1
className="absolute inset-0 text-6xl md:text-8xl lg:text-9xl font-black select-none"
style={{
WebkitTextStroke: "2px #64748b",
WebkitTextFillColor: "transparent",
}}
animate={{
x: isHovered ? -4 : 0,
y: isHovered ? -4 : 0,
opacity: isHovered ? 0.8 : 0,
}}
transition={{ duration: 0.3, ease: "easeOut" }}
>
{text}
</motion.h1>
{/* Particles on hover */}
{isHovered && Array.from({ length: 15 }).map((_, i) => (
<motion.div
key={i}
className="absolute w-2 h-2 bg-slate-500 rounded-full"
style={{
left: `${Math.random() * 100}%`,
top: `${Math.random() * 100}%`,
}}
initial={{ opacity: 0, scale: 0 }}
animate={{
opacity: [0, 1, 0],
scale: [0, 1, 0],
x: [0, (Math.random() - 0.5) * 100],
y: [0, (Math.random() - 0.5) * 100],
}}
transition={{
duration: 1.5,
ease: "easeOut",
delay: Math.random() * 0.5,
}}
/>
))}
{/* Background glow */}
<motion.div
className="absolute inset-0 bg-gradient-radial from-slate-300/20 to-transparent rounded-full blur-3xl -z-20"
animate={{
scale: isHovered ? 1.5 : 0,
opacity: isHovered ? 1 : 0,
}}
transition={{ duration: 0.5, ease: "easeOut" }}
/>
</div>
</div>
);
}
export default TextHover;
"use client";
import React, { useState } from "react";
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
export function TextHover() {
const [isHovered, setIsHovered] = useState(false);
const text = "UI STUDIO";
return (
<div className="h-[40rem] flex items-center justify-center bg-slate-50 dark:bg-slate-950">
<div
className="relative cursor-pointer"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{/* Main text */}
<motion.h1
className="text-6xl md:text-8xl lg:text-9xl font-black text-slate-900 dark:text-slate-100 select-none"
animate={{
scale: isHovered ? 1.05 : 1,
}}
transition={{ duration: 0.3, ease: "easeOut" }}
>
{text.split("").map((letter, index) => (
<motion.span
key={index}
className="inline-block"
animate={{
y: isHovered ? Math.sin(index * 0.5) * 10 : 0,
color: isHovered ? "#64748b" : undefined,
}}
transition={{
duration: 0.4,
delay: isHovered ? index * 0.05 : 0,
ease: "easeOut",
}}
>
{letter === " " ? "\u00A0" : letter}
</motion.span>
))}
</motion.h1>
{/* Shadow text */}
<motion.h1
className="absolute inset-0 text-6xl md:text-8xl lg:text-9xl font-black text-slate-400 dark:text-slate-600 select-none -z-10"
animate={{
x: isHovered ? 8 : 0,
y: isHovered ? 8 : 0,
opacity: isHovered ? 0.5 : 0,
}}
transition={{ duration: 0.3, ease: "easeOut" }}
>
{text}
</motion.h1>
{/* Outline text */}
<motion.h1
className="absolute inset-0 text-6xl md:text-8xl lg:text-9xl font-black select-none"
style={{
WebkitTextStroke: "2px #64748b",
WebkitTextFillColor: "transparent",
}}
animate={{
x: isHovered ? -4 : 0,
y: isHovered ? -4 : 0,
opacity: isHovered ? 0.8 : 0,
}}
transition={{ duration: 0.3, ease: "easeOut" }}
>
{text}
</motion.h1>
{/* Particles on hover */}
{isHovered && Array.from({ length: 15 }).map((_, i) => (
<motion.div
key={i}
className="absolute w-2 h-2 bg-slate-500 rounded-full"
style={{
left: `${Math.random() * 100}%`,
top: `${Math.random() * 100}%`,
}}
initial={{ opacity: 0, scale: 0 }}
animate={{
opacity: [0, 1, 0],
scale: [0, 1, 0],
x: [0, (Math.random() - 0.5) * 100],
y: [0, (Math.random() - 0.5) * 100],
}}
transition={{
duration: 1.5,
ease: "easeOut",
delay: Math.random() * 0.5,
}}
/>
))}
{/* Background glow */}
<motion.div
className="absolute inset-0 bg-gradient-radial from-slate-300/20 to-transparent rounded-full blur-3xl -z-20"
animate={{
scale: isHovered ? 1.5 : 0,
opacity: isHovered ? 1 : 0,
}}
transition={{ duration: 0.5, ease: "easeOut" }}
/>
</div>
</div>
);
}
export default TextHover;
4
Update the import paths to match your project setup
Props
Prop | Type | Default | Description |
---|---|---|---|
text | string | UI STUDIO | The text to display with hover effects. |
className | string | Additional CSS classes for the text hover container. |