Command Palette

Search for a command to run...

Text Morph

An interactive collection of stacked headings that reveal individual content sections with a click.

Installation

Install the following dependencies:

pnpm add motion

Create a file at components/motion/text-morph.tsx and paste the given code.

"use client";
import { cn } from "@/lib/utils";
import { AnimatePresence, motion, Transition, Variants } from "motion/react";
import { useMemo, useId } from "react";
 
export type TextMorphProps = {
    children: string;
    as?: React.ElementType;
    className?: string;
    style?: React.CSSProperties;
    variants?: Variants;
    transition?: Transition;
};
 
export function TextMorph({
    children,
    as: Component = "p",
    className,
    style,
    variants,
    transition,
}: TextMorphProps) {
    const uniqueId = useId();
 
    const characters = useMemo(() => {
        const charCounts: Record<string, number> = {};
 
        return children.split("").map((char, index) => {
            const lowerChar = char.toLowerCase();
            charCounts[lowerChar] = (charCounts[lowerChar] || 0) + 1;
 
            return {
                id: `${uniqueId}-${lowerChar}${charCounts[lowerChar]}`,
                label:
                    char === " "
                        ? "\u00A0"
                        : index === 0
                          ? char.toUpperCase()
                          : lowerChar,
            };
        });
    }, [children, uniqueId]);
 
    const defaultVariants: Variants = {
        initial: { opacity: 0 },
        animate: { opacity: 1 },
        exit: { opacity: 0 },
    };
 
    const defaultTransition: Transition = {
        type: "spring",
        stiffness: 280,
        damping: 18,
        mass: 0.3,
    };
 
    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.