Docs
Wave Reveal
Reveal letter or word one by one with a wave effect & optional blur effect.
Installation
Update tailwind.config.js
theme: {
extend: {
transitionTimingFunction: {
"minor-spring": "cubic-bezier(0.18,0.89,0.82,1.04)",
},
keyframes:{
"reveal-up": {
"0%": { opacity: "0", transform: "translateY(80%)" },
"100%": { opacity: "1", transform: "translateY(0)" },
},
"reveal-down": {
"0%": { opacity: "0", transform: "translateY(-80%)" },
"100%": { opacity: "1", transform: "translateY(0)" },
},
"content-blur": {
"0%": { filter: "blur(0.3rem)" },
"100%": { filter: "blur(0)" },
},
}
},
},
Run the following command
It will create a new file called wave-reveal.tsx
inside the components/animata/text
directory.
touch components/animata/text/wave-reveal.tsx
Paste the code
Open the newly created file and paste the following code:
import { ReactNode } from "react";
import { cn } from "@/lib/utils";
interface WaveRevealProps {
/**
* The text to animate
*/
text: string;
/**
* Additional classes for the container
*/
className?: string;
/**
* The direction of the animation
* @default "down"
*/
direction?: "up" | "down";
/**
* The mode of the animation
* @default "letter"
*/
mode?: "letter" | "word";
/**
* Duration of the animation
* E.g. 2000ms
*/
duration?: string;
/**
* If true, the text will apply a blur effect as seen in WWDC.
*/
blur?: boolean;
letterClassName?: string;
/**
* Delay for each letter/word in ms
*/
delay?: number;
}
interface ReducedValue extends Pick<WaveRevealProps, "direction" | "mode"> {
nodes: ReactNode[];
offset: number;
duration: number | string;
delay: number;
blur?: boolean;
className?: string;
wordsLength: number;
textLength: number;
}
const Word = ({
isWordMode,
word,
index,
offset,
delay,
duration,
className,
}: Pick<ReducedValue, "delay" | "duration" | "offset"> & {
index: number;
className: string;
isWordMode: boolean;
word: string;
length: number;
}) => {
if (isWordMode) {
return word;
}
return (
<>
{word.split("").map((letter, letterIndex) => {
return (
<span
key={`${letter}_${letterIndex}_${index}`}
className={cn({
[className]: !isWordMode,
})}
style={{
animationDuration: `${duration}`,
animationDelay: createDelay({
index: letterIndex,
offset,
delay,
}),
}}
>
{letter}
</span>
);
})}
</>
);
};
const createDelay = ({
offset,
index,
delay,
}: Pick<ReducedValue, "offset" | "delay"> & {
index: number;
}) => {
return delay + (index + offset) * 50 + "ms";
};
const createAnimatedNodes = (args: ReducedValue, word: string, index: number): ReducedValue => {
const { nodes, offset, wordsLength, textLength, mode, direction, duration, delay, blur } = args;
const isWordMode = mode === "word";
const isUp = direction === "up";
const length = isWordMode ? wordsLength : textLength;
const isLast = index === length - 1;
const className = cn(
"inline-block opacity-0 transition-all ease-in-out fill-mode-forwards",
{
// Determine the animation direction
["animate-[reveal-down]"]: !isUp && !blur,
["animate-[reveal-up]"]: isUp && !blur,
["animate-[reveal-down,content-blur]"]: !isUp && blur,
["animate-[reveal-up,content-blur]"]: isUp && blur,
},
args.className,
);
const node = (
<span
key={`word_${index}`}
className={cn("contents", {
[className]: isWordMode,
})}
style={
isWordMode
? {
animationDuration: `${duration}`,
animationDelay: createDelay({
index,
offset,
delay,
}),
}
: undefined
}
>
<Word
isWordMode={isWordMode}
word={word}
index={index}
offset={offset}
duration={duration}
className={className}
length={length}
delay={delay}
/>
{!isLast && " "}
</span>
);
return {
...args,
nodes: [...nodes, node],
offset: offset + (isWordMode ? 1 : word.length + 1),
};
};
export default function WaveReveal({
text,
direction = "down",
mode = "letter",
className,
duration = "2000ms",
delay = 0,
blur = true,
letterClassName,
}: WaveRevealProps) {
if (!text) {
return null;
}
const words = text.trim().split(/\s/);
const { nodes } = words.reduce<ReducedValue>(createAnimatedNodes, {
nodes: [],
offset: 0,
wordsLength: words.length,
textLength: text.length,
direction,
mode,
duration: duration ?? 60,
delay: delay ?? 0,
blur,
className: letterClassName,
});
return (
<div
className={cn(
"relative flex flex-wrap justify-center whitespace-pre px-2 text-4xl font-black md:px-6 md:text-7xl",
className,
)}
>
{nodes}
<div className="sr-only">{text}</div>
</div>
);
}
Credits
Built by hari