MorphoText Flip

PreviousNext

Animated text component that flips between words with smooth transitions.

Docs
scrollxuicomponent

Preview

Loading preview…
components/ui/morphotextflip.tsx
"use client";
import React, { useState, useEffect, useId } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { cn } from "@/lib/utils";

export interface MorphoTextFlipProps {
  words?: string[];
  interval?: number;
  className?: string;
  textClassName?: string;
  animationDuration?: number;
  animationType?: "slideUp" | "fadeScale" | "flipY" | "slideRotate" | "elastic";
}

export function MorphoTextFlip({
  words = ["remarkable", "bold", "scalable", "beautiful"],
  interval = 3000,
  className,
  textClassName,
  animationDuration = 700,
  animationType = "slideUp",
}: MorphoTextFlipProps) {
  const id = useId();
  const [currentWordIndex, setCurrentWordIndex] = useState(0);
  const [width, setWidth] = useState("auto");
  const textRef = React.useRef<HTMLDivElement>(null);
  const measureRef = React.useRef<HTMLDivElement>(null);

  const updateWidthForWord = () => {
    if (measureRef.current) {
      const textWidth = measureRef.current.scrollWidth + 48;
      setWidth(`${textWidth}px`);
    }
  };

  useEffect(() => {
    const timer = setTimeout(() => {
      updateWidthForWord();
    }, 10);
    return () => clearTimeout(timer);
  }, [currentWordIndex]);

  useEffect(() => {
    const intervalId = setInterval(() => {
      setCurrentWordIndex((prevIndex) => (prevIndex + 1) % words.length);
    }, interval);
    return () => clearInterval(intervalId);
  }, [words, interval]);

  const animationVariants = {
    slideUp: {
      initial: { y: 40, opacity: 0 },
      animate: { y: 0, opacity: 1 },
      exit: { y: -40, opacity: 0 },
    },
    fadeScale: {
      initial: { scale: 0.8, opacity: 0 },
      animate: { scale: 1, opacity: 1 },
      exit: { scale: 1.2, opacity: 0 },
    },
    flipY: {
      initial: { rotateY: 90, opacity: 0 },
      animate: { rotateY: 0, opacity: 1 },
      exit: { rotateY: -90, opacity: 0 },
    },
    slideRotate: {
      initial: { x: 100, rotate: 10, opacity: 0 },
      animate: { x: 0, rotate: 0, opacity: 1 },
      exit: { x: -100, rotate: -10, opacity: 0 },
    },
    elastic: {
      initial: { scale: 0, rotate: -180 },
      animate: { scale: 1, rotate: 0 },
      exit: { scale: 0, rotate: 180 },
    },
  };

  const currentVariant = animationVariants[animationType];
  const duration = animationDuration / 1000;

  return (
    <motion.div
      layout
      layoutId={`words-container-${id}`}
      animate={{ width }}
      transition={{
        duration: duration * 0.4,
        ease: "easeInOut",
        type: "spring",
        stiffness: 300,
        damping: 30,
      }}
      className={cn(
        "relative inline-block overflow-hidden rounded-2xl px-6 pt-2 pb-3 backdrop-blur-sm border border-gray-200 shadow-xl bg-white/70 dark:bg-slate-800/70 dark:border-slate-700",
        className
      )}
    >
      <div className="relative flex items-center justify-center">
        <div
          ref={measureRef}
          className={cn(
            "absolute opacity-0 pointer-events-none whitespace-nowrap",
            textClassName || "text-4xl font-bold md:text-7xl"
          )}
          style={{ top: -9999 }}
        >
          {words[currentWordIndex]}
        </div>

        <AnimatePresence mode="wait">
          <motion.div
            key={words[currentWordIndex]}
            initial={currentVariant.initial}
            animate={currentVariant.animate}
            exit={currentVariant.exit}
            transition={{
              duration: duration * 0.6,
              ease:
                animationType === "elastic"
                  ? [0.68, -0.55, 0.265, 1.55]
                  : "easeInOut",
            }}
            className={cn(
              "whitespace-nowrap",
              textClassName || "text-4xl font-bold text-rose-600 dark:text-rose-400 md:text-7xl"
            )}
            ref={textRef}
          >
            {words[currentWordIndex]}
          </motion.div>
        </AnimatePresence>
      </div>
    </motion.div>
  );
}

Installation

npx shadcn@latest add @scrollxui/morphotextflip

Usage

import { Morphotextflip } from "@/components/morphotextflip"
<Morphotextflip />