Smooth text transitions for React that morph between words character by character. Built for headings and hero sections.
import { MorphText } from "@/components/morph-text";
const Demo: React.FC = () => {
const [installed, setInstalled] = useState(false);
return (
<Button variant="secondary" onClick={() => setInstalled(!installed)}>
<MorphText text={installed ? "Installed" : "Not Installed"} />
</Button>
);
};npm install motionbun add motiondeno add motionpnpm add motionyarn add motionCopy and paste the following code into your project.
"use client";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { useId, useMemo } from "react";
type MorphTextProps<T extends React.ElementType = "span"> = {
text: string;
as?: T;
} & React.ComponentProps<T>;
const MorphText = <T extends React.ElementType = "span">({
text,
as,
...props
}: MorphTextProps<T>) => {
// Generate a random id to avoid key conflicts between multiple instances
const randomId = useId();
const textToDisplay = useMemo(
() => generateKeys(text, randomId),
[text, randomId]
);
const shouldReduceMotion = useReducedMotion();
const Comp = as || "span";
return (
<Comp {...props}>
<AnimatePresence initial={false} mode="popLayout">
{textToDisplay.map(({ char, key }) => (
<motion.span
animate={{ opacity: 1 }}
className="inline-block"
exit={{ opacity: 0 }}
initial={{ opacity: 0 }}
key={key}
layoutId={key}
transition={
shouldReduceMotion
? { duration: 0 }
: {
bounce: 0,
duration: 0.25,
opacity: {
bounce: 0,
duration: 0.35,
type: "spring",
},
type: "spring",
}
}
>
{char === " " ? "\u00A0" : char || ""}
</motion.span>
))}
</AnimatePresence>
</Comp>
);
};
function generateKeys(text: string, randomId: string) {
const charCount: { [key: string]: number } = {};
return text.split("").map((char) => {
if (!charCount[char]) {
charCount[char] = 0;
}
const key = `${char}-${charCount[char]}-${randomId}`;
charCount[char]++;
return { char, key };
});
}
export { MorphText };"use client";
const textes = ["Cool", "Animated", "Useless", "Awesome", "Boring"];
const [text, setText] = useState(textes[0]);
useEffect(() => {
const interval = setInterval(() => {
setText(textes[Math.floor(Math.random() * textes.length)]);
}, 1000);
return () => clearInterval(interval);
}, []);
return (
<MorphText text={text} />
);
<MorphText as="a" text="Hello" href="my-cool-link.mom" />It accepts all the props of the HTML element it renders as, plus the following:
| Prop | Type | Description |
|---|---|---|
text | string | The text to display. |
as | React.ElementType | The HTML element to render as. Default is span. |