Components
Gooey Text Morphing
A smooth, gooey text morphing animation between multiple strings.
Last updated on
Gooey Text Morphing
"use client";
import { GooeyText } from "@/components/ui/gooey-text-morphing";
export function GooeyTextMorphingDemo() {
const texts = ["Why", "is", "it", "so", "satisfying", "to", "watch?"];
return (
<div className="flex h-[300px] w-full items-center justify-center overflow-hidden rounded-xl border bg-background">
<GooeyText
texts={texts}
morphTime={1}
cooldownTime={0.5}
textClassName="font-bold tracking-tighter"
/>
</div>
);
}Installation
CLI
npx shadcn@latest add "https://jolyui.dev/r/gooey-text-morphing"Manual
Copy and paste the following code into your project. component/ui/gooey-text-morphing.tsx
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
interface GooeyTextProps {
texts: string[];
morphTime?: number;
cooldownTime?: number;
className?: string;
textClassName?: string;
}
export function GooeyText({
texts,
morphTime = 1,
cooldownTime = 0.25,
className,
textClassName,
}: GooeyTextProps) {
const text1Ref = React.useRef<HTMLSpanElement>(null);
const text2Ref = React.useRef<HTMLSpanElement>(null);
React.useEffect(() => {
let textIndex = texts.length - 1;
let time = new Date();
let morph = 0;
let cooldown = cooldownTime;
const setMorph = (fraction: number) => {
if (text1Ref.current && text2Ref.current) {
text2Ref.current.style.filter = `blur(${Math.min(8 / fraction - 8, 100)}px)`;
text2Ref.current.style.opacity = `${fraction ** 0.4 * 100}%`;
fraction = 1 - fraction;
text1Ref.current.style.filter = `blur(${Math.min(8 / fraction - 8, 100)}px)`;
text1Ref.current.style.opacity = `${fraction ** 0.4 * 100}%`;
}
};
const doCooldown = () => {
morph = 0;
if (text1Ref.current && text2Ref.current) {
text2Ref.current.style.filter = "";
text2Ref.current.style.opacity = "100%";
text1Ref.current.style.filter = "";
text1Ref.current.style.opacity = "0%";
}
};
const doMorph = () => {
morph -= cooldown;
cooldown = 0;
let fraction = morph / morphTime;
if (fraction > 1) {
cooldown = cooldownTime;
fraction = 1;
}
setMorph(fraction);
};
function animate() {
requestAnimationFrame(animate);
const newTime = new Date();
const shouldIncrementIndex = cooldown > 0;
const dt = (newTime.getTime() - time.getTime()) / 1000;
time = newTime;
cooldown -= dt;
if (cooldown <= 0) {
if (shouldIncrementIndex) {
textIndex = (textIndex + 1) % texts.length;
if (text1Ref.current && text2Ref.current) {
text1Ref.current.textContent =
texts[textIndex % texts.length] ?? "";
text2Ref.current.textContent =
texts[(textIndex + 1) % texts.length] ?? "";
}
}
doMorph();
} else {
doCooldown();
}
}
animate();
return () => {
// Cleanup function if needed
};
}, [texts, morphTime, cooldownTime]);
return (
<div className={cn("relative", className)}>
<svg className="absolute h-0 w-0" aria-hidden="true" focusable="false">
<defs>
<filter id="threshold">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
0 0 0 255 -140"
/>
</filter>
</defs>
</svg>
<div
className="flex items-center justify-center"
style={{ filter: "url(#threshold)" }}
>
<span
ref={text1Ref}
className={cn(
"absolute inline-block select-none text-center text-6xl md:text-[60pt]",
"text-foreground",
textClassName,
)}
/>
<span
ref={text2Ref}
className={cn(
"absolute inline-block select-none text-center text-6xl md:text-[60pt]",
"text-foreground",
textClassName,
)}
/>
</div>
</div>
);
}Usage
import { GooeyText } from "@/components/ui/gooey-text-morphing";
function MyComponent() {
const texts = ["Hello", "World", "Gooey", "Morph"];
return (
<GooeyText
texts={texts}
morphTime={1}
cooldownTime={0.5}
/>
);
}API Reference
Prop
Type
Notes
- The component uses SVG filters to achieve the "gooey" effect.
- The
textsarray can contain any number of strings. morphTimecontrols the duration of the transition between words.cooldownTimecontrols how long each word stays visible before morphing again.- Ensure the container has enough height to accommodate the text size.
Credits
Inspired by Victor Welander.
How is this guide?