Spell UI
componentblur-revealsource: ok · hit
Blur Reveal
Animated text reveal with blur effect.
npx shadcn add "https://spell.sh/r/blur-reveal.json"free
Visual style
glass-blur
Interaction
scroll-reveal
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · mediumanimationmotion
Files1 · source cached 10/14/2026, 1:09:01 PM
registry/spell-ui/blur-reveal.tsx
"use client"
import { AnimatePresence, motion } from "motion/react"
import type React from "react"
export interface BlurRevealProps {
children: string
className?: string
delay?: number
speedReveal?: number
speedSegment?: number
trigger?: boolean
onAnimationComplete?: () => void
onAnimationStart?: () => void
as?: keyof React.JSX.IntrinsicElements
style?: React.CSSProperties
inView?: boolean
once?: boolean
letterSpacing?: string | number
}
export function BlurReveal({
children,
className,
delay = 0,
speedReveal = 1.5,
speedSegment = 0.5,
trigger = true,
onAnimationComplete,
onAnimationStart,
as = "p",
style,
inView = false,
once = true,
letterSpacing,
}: BlurRevealProps) {
const MotionTag = motion[as as keyof typeof motion] as typeof motion.div
const stagger = 0.03 / speedReveal
const baseDuration = 0.3 / speedSegment
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: stagger,
delayChildren: delay,
},
},
exit: {
transition: {
staggerChildren: stagger,
staggerDirection: -1,
},
},
}
const itemVariants = {
hidden: { opacity: 0, filter: "blur(12px)", y: 10 },
visible: {
opacity: 1,
filter: "blur(0px)",
y: 0,
transition: {
duration: baseDuration,
},
},
exit: { opacity: 0, filter: "blur(12px)", y: 10 },
}
return (
<AnimatePresence mode="popLayout">
{trigger && (
<MotionTag
initial="hidden"
whileInView={inView ? "visible" : undefined}
animate={inView ? undefined : "visible"}
exit="exit"
variants={containerVariants}
viewport={{ once }}
className={className}
onAnimationComplete={onAnimationComplete}
onAnimationStart={onAnimationStart}
style={style}
>
<span className="sr-only">{children}</span>
{children &&
children.split(" ").map((word, wordIndex, wordsArray) => (
<span key={`word-${wordIndex}`} className="inline-block whitespace-nowrap" aria-hidden="true">
{word.split("").map((char, charIndex) => (
<motion.span
key={`char-${wordIndex}-${charIndex}`}
variants={itemVariants}
className="inline-block"
style={letterSpacing ? { marginRight: letterSpacing } : undefined}
>
{char}
</motion.span>
))}
{wordIndex < wordsArray.length - 1 && (
<motion.span
key={`space-${wordIndex}`}
variants={itemVariants}
className="inline-block"
>
</motion.span>
)}
</span>
))}
</MotionTag>
)}
</AnimatePresence>
)
}