Back

Morphing Text

Text that smoothly animates character changes when the value updates.

Category
TextReact
CSS
Tailwind

Manual

Create a file and paste the following code into it.

components/ui/morphing-text.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
"use client";

import * as React from "react";
import { motion, AnimatePresence, Transition, Variants } from "motion/react";
import { cn } from "@/lib/cn";

export type MorphingTextProps = {
  children: string;
  as?: React.ElementType;
  className?: string;
  style?: React.CSSProperties;
  variants?: Variants;
  transition?: Transition;
};

export default function MorphingText({
  children,
  as: Component = "span",
  className,
  style,
  variants,
  transition,
}: MorphingTextProps) {
  const uniqueId = React.useId();

  const characters = React.useMemo(() => {
    const charCounts: Record<string, number> = {};

    return children.split("").map((char) => {
      const lowerChar = char.toLowerCase();
      charCounts[lowerChar] = (charCounts[lowerChar] || 0) + 1;

      return {
        id: `${uniqueId}-${lowerChar}${charCounts[lowerChar]}`,
        label: char === " " ? " " : char,
      };
    });
  }, [children, uniqueId]);

  const defaultVariants: Variants = {
    initial: { opacity: 0, y: -20, filter: "blur(8px)", scale: 0.8 },
    animate: { opacity: 1, y: 0, filter: "blur(0px)", scale: 1 },
    exit: { opacity: 0, y: 20, filter: "blur(8px)", scale: 0.8 },
  };

  const defaultTransition: Transition = {
    duration: 0.3,
    ease: [0.4, 0, 0.2, 1],
  };

  return (
    <Component className={cn(className)} aria-label={children} style={style}>
      <AnimatePresence mode="popLayout" initial={false}>
        {characters.map((character) => (
          <motion.span
            key={character.id}
            layoutId={character.id}
            className="inline-block"
            aria-hidden="true"
            initial="initial"
            animate="animate"
            exit="exit"
            variants={variants || defaultVariants}
            transition={transition || defaultTransition}
          >
            {character.label}
          </motion.span>
        ))}
      </AnimatePresence>
    </Component>
  );
}

Update the import paths to match your project setup.

Similar screens