Docs

Text Explode (iMessage)

Text explode effect as seen in iMessage

Installation

Install dependencies

npm install framer-motion

Run the following command

It will create a new file text-explode-imessage.tsx inside the components/animata/text directory.

mkdir -p components/animata/text && touch components/animata/text/text-explode-imessage.tsx

Paste the code

Open the newly created file and paste the following code:

import { useCallback, useEffect, useRef } from "react";
import { motion, useAnimationControls, Variants } from "framer-motion";
 
import { cn } from "@/lib/utils";
 
const containerVariants: Variants = {
  initial: {
    opacity: 1,
    translateY: 0,
    transition: {
      duration: 0.5,
    },
    letterSpacing: "0px",
  },
  shrink: {
    // Scale might need to be adjust according to font size for better effect
    scale: 0.8,
    letterSpacing: "-10%",
  },
  jitter: {
    x: [0, -3, 3, -3, 3, 0],
    y: [0, -2, 2, -2, 2, 0],
    transition: {
      duration: 0.5,
      times: [0, 0.2, 0.4, 0.6, 0.8, 1],
      ease: "easeInOut",
    },
  },
  explode: {
    scale: [0.7, 0.9, 1],
    opacity: [1, 0.7, 0],
    letterSpacing: "0px",
    transition: {
      times: [0, 0.9, 1],
    },
  },
  end: {
    scale: 1,
    letterSpacing: "0px",
    translateY: 50,
  },
};
 
const createExplosion = ({ index, total }: { index: number; total: number }) => {
  const direction = Math.random() > Math.random() ? -1 : 1;
 
  const x = Math.random() * 10 * total * direction;
 
  const radius = total * 4;
  const angleRange = Math.PI;
  const angle = (index / (total - 1)) * angleRange;
  const y = radius * -Math.sin(angle) * Math.random();
 
  const rotation = Math.random() * 360 * direction;
 
  return {
    translateX: [0, x * 0.5, x * 0.7, x],
    translateY: [0, y, -y / 5, 0, 5],
    rotate: [0, rotation * 0.4, rotation * 0.8, rotation],
    scale: [0.9, 1.2, 1 + Math.random() + 0.2, 1 + Math.random() * 2],
    opacity: [1, 0.8, 0.5, 0],
  };
};
 
const characterVariants: Variants = {
  jitter: () => ({
    x: [0, -3 + Math.random() * 6, 3 - Math.random() * 6, 0],
    y: [0, -2 + Math.random() * 4, 2 - Math.random() * 4, 0],
    transition: {
      duration: 0.5,
      times: [0, 0.33, 0.66, 1],
      ease: "easeInOut",
    },
  }),
  shrink: {
    scale: 1.1,
  },
  explode: createExplosion,
  end: {
    translateY: 0,
    translateX: 0,
    rotate: 0,
    scale: 1,
  },
  initial: {
    opacity: 1,
  },
};
 
const splitText = (text: string) => String(text).split(/(?:)/u);
 
export default function TextExplodeIMessage({
  text,
  mode = "loop",
  className,
}: {
  text: string;
  className?: string;
  mode?: "loop" | "hover";
}) {
  const characters = splitText(text);
  const controls = useAnimationControls();
  const isPlaying = useRef(false);
 
  const animateSequence = useCallback(async () => {
    await Promise.all([
      controls.start("shrink", {
        duration: 1,
        ease: "easeOut",
      }),
      controls.start("jitter", {
        delay: 0.1,
      }),
    ]);
    await controls.start("explode", {});
    await controls.start("end");
    await controls.start("initial", {
      delay: 0.5,
      duration: 1,
      type: "spring",
    });
 
    if (mode === "loop") {
      requestAnimationFrame(() => animateSequence());
    } else {
      isPlaying.current = false;
    }
  }, [mode, controls]);
 
  useEffect(() => {
    if (!characters.length || mode === "hover") {
      return;
    }
 
    animateSequence();
  }, [characters.length, mode, animateSequence]);
 
  return (
    <motion.div
      variants={containerVariants}
      animate={controls}
      onPointerDown={() => {
        if (mode === "hover" && !isPlaying.current) {
          isPlaying.current = true;
          animateSequence();
        }
      }}
      onMouseEnter={() => {
        if (mode === "hover" && !isPlaying.current) {
          isPlaying.current = true;
          animateSequence();
        }
      }}
      className={cn(
        "flex items-center justify-center text-3xl tracking-normal text-foreground",
        className,
      )}
    >
      {characters.map((char, index) => (
        <motion.span
          key={index}
          variants={characterVariants}
          custom={{ index, total: characters.length }}
          className="inline-block"
        >
          {char === " " ? "\u00A0" : char}
        </motion.span>
      ))}
      <span className="sr-only">{text}</span>
    </motion.div>
  );
}

Credits

Built by hari