An engaging text loading animation component that reveals words with smooth transitions, perfect for splash screens and content reveals.
"use client"
import { useEffect, useState } from "react"
import { motion, AnimatePresence, useReducedMotion } from "motion/react"
const words = ["Hello", "Hola", "Bonjour", "Hallo", "Ciao", "नमस्ते"]
// Accelerating rhythm — words appear faster as sequence builds momentum
const wordDurations = [650, 600, 550, 520, 500, 800]
// ease-out-quint: snappy entrance, smooth settle
const easeOutQuint = [0.23, 1, 0.32, 1] as const
export default function WordPreload() {
const [index, setIndex] = useState(0)
const [finished, setFinished] = useState(false)
const [showLogo, setShowLogo] = useState(false)
const shouldReduceMotion = useReducedMotion()
// Cycle through words with accelerating rhythm
useEffect(() => {
if (finished) return
if (index < words.length - 1) {
const timer = setTimeout(() => setIndex((i) => i + 1), wordDurations[index])
return () => clearTimeout(timer)
} else {
const timer = setTimeout(() => {
setShowLogo(true)
setTimeout(() => setFinished(true), 1500)
}, wordDurations[words.length - 1])
return () => clearTimeout(timer)
}
}, [index, finished])
return (
<div className="relative w-full h-[800px] bg-background text-foreground flex items-center justify-center overflow-hidden">
<div className="relative w-full h-full flex items-center justify-center">
{/* Word cycle */}
<AnimatePresence mode="wait">
{!showLogo && (
<motion.div
key={index}
initial={shouldReduceMotion ? false : { opacity: 0, scale: 0.94, y: 14 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.96, y: -10 }}
transition={{
duration: 0.38,
exit: { duration: 0.3 },
ease: easeOutQuint,
}}
className="absolute text-7xl font-light tracking-tight text-foreground will-change-transform"
style={{ fontFamily: "system-ui, -apple-system, sans-serif" }}
>
{words[index]}
</motion.div>
)}
</AnimatePresence>
{/* Logo reveal */}
<AnimatePresence>
{showLogo && !finished && (
<motion.div
initial={shouldReduceMotion ? false : { opacity: 0, scale: 0.85 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 1.03 }}
transition={{
duration: 0.55,
ease: easeOutQuint,
}}
className="absolute flex flex-col items-center space-y-6 will-change-transform"
>
<div className="w-16 h-16 text-foreground">
<svg viewBox="0 0 24 24" fill="currentColor" className="w-full h-full">
<path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" />
</svg>
</div>
{/* Loading dots */}
<div className="flex space-x-1.5">
{[0, 1, 2].map((i) => (
<motion.div
key={i}
className="w-2 h-2 bg-muted-foreground rounded-full"
animate={{
scale: [1, 1.3, 1],
opacity: [0.3, 1, 0.3],
}}
transition={{
duration: 1.2,
repeat: Infinity,
delay: i * 0.15,
ease: [0.45, 0, 0.55, 1],
}}
/>
))}
</div>
</motion.div>
)}
</AnimatePresence>
{/* Screen reveal */}
<AnimatePresence>
{finished && (
<motion.div
initial={{
clipPath: "ellipse(0% 0% at 50% 100%)",
opacity: 0,
}}
animate={{
clipPath: "ellipse(150% 150% at 50% 100%)",
opacity: 1,
}}
transition={{
duration: 0.9,
ease: easeOutQuint,
delay: 0.12,
}}
className="absolute inset-0 w-full h-full bg-neutral-800 flex flex-col items-center justify-center overflow-hidden"
>
<motion.div
initial={shouldReduceMotion ? false : { opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.6,
delay: 0.5,
ease: easeOutQuint,
}}
className="text-center space-y-6 max-w-2xl px-8 will-change-transform"
>
<h1
className="text-6xl md:text-7xl font-semibold tracking-tight text-foreground text-balance"
style={{ fontFamily: "system-ui, -apple-system, sans-serif" }}
>
Your Vibe Coded Landing Page
</h1>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
)
}