Components

Character Morph

A character-by-character text morphing component with staggered animations.

Last updated on

Edit on GitHub
"use client";
 
import { CharacterMorph } from "@/components/ui/character-morph";
 
export function CharacterMorphDemo() {
  return (
    <div className="flex items-center justify-center">
      <CharacterMorph
        texts={["Hello", "World", "Morph", "Effect"]}
        className="font-bold text-4xl text-foreground"
      />
    </div>
  );
}

Examples

Variants

"use client";
 
import { CharacterMorph } from "@/components/ui/character-morph";
 
export function CharacterMorphCustomDemo() {
  return (
    <div className="flex flex-col items-center justify-center gap-8">
      <div className="text-center">
        <h3 className="mb-4 font-semibold text-lg">Fast Morph</h3>
        <CharacterMorph
          texts={["Code", "Hack", "Debug", "Fix"]}
          interval={2000}
          staggerDelay={0.02}
          charDuration={0.3}
          className="font-mono text-2xl text-primary"
        />
      </div>
 
      <div className="text-center">
        <h3 className="mb-4 font-semibold text-lg">Slow Morph</h3>
        <CharacterMorph
          texts={["Design", "Create", "Build", "Deploy"]}
          interval={4000}
          staggerDelay={0.05}
          charDuration={0.8}
          className="font-bold text-3xl text-destructive"
        />
      </div>
    </div>
  );
}

Installation

CLI

npx shadcn@latest add "https://jolyui.dev/r/character-morph"

Manual

Install the following dependencies:

npm install motion

Copy and paste the following code into your project. component/ui/character-morph.tsx

import { AnimatePresence, motion } from "motion/react";
import * as React from "react";
import { cn } from "@/lib/utils";
 
interface CharacterMorphProps {
  texts: string[];
  className?: string;
  interval?: number;
  staggerDelay?: number;
  charDuration?: number;
}
 
const CharacterMorph = React.forwardRef<HTMLDivElement, CharacterMorphProps>(
  (
    {
      texts,
      className,
      interval = 3000,
      staggerDelay = 0.03,
      charDuration = 0.5,
    },
    ref,
  ) => {
    const [currentIndex, setCurrentIndex] = React.useState(0);
    const currentText = texts[currentIndex] || "";
 
    React.useEffect(() => {
      const timer = setInterval(() => {
        setCurrentIndex((prev) => (prev + 1) % texts.length);
      }, interval);
 
      return () => clearInterval(timer);
    }, [interval, texts.length]);
 
    const maxLength = Math.max(...texts.map((t) => t.length));
 
    return (
      <div
        ref={ref}
        className={cn("relative inline-flex whitespace-nowrap", className)}
      >
        <AnimatePresence mode="popLayout">
          {currentText.split("").map((char, i) => (
            <motion.span
              key={`${currentIndex}-${i}-${char}`}
              initial={{ opacity: 0, y: 20, filter: "blur(8px)", rotateX: -90 }}
              animate={{ opacity: 1, y: 0, filter: "blur(0px)", rotateX: 0 }}
              exit={{ opacity: 0, y: -20, filter: "blur(8px)", rotateX: 90 }}
              transition={{
                duration: charDuration,
                delay: i * staggerDelay,
                ease: [0.215, 0.61, 0.355, 1],
              }}
              className="inline-block"
              style={{ transformStyle: "preserve-3d" }}
            >
              {char === " " ? "\u00A0" : char}
            </motion.span>
          ))}
        </AnimatePresence>
        {/* Maintain minimum width */}
        <span className="invisible absolute">{"M".repeat(maxLength)}</span>
      </div>
    );
  },
);
 
CharacterMorph.displayName = "CharacterMorph";
export { CharacterMorph };

API Reference

Prop

Type

Notes

  • Uses motion/react for smooth character-by-character animations
  • Each character animates with opacity, vertical movement, blur, and 3D rotation
  • Staggered animation delays create a wave-like morphing effect
  • Automatically cycles through texts at specified intervals
  • Supports custom CSS classes for styling
  • Maintains consistent width by using the longest text as reference
  • Perfect for dynamic headlines, loading states, or eye-catching text effects

How is this guide?

On this page