shadimage
ScrollX UI
uianimated-textgeneratesource: ok · hit

Animated TextGenerate

Generates animated text word-by-word with blur, highlights, links, and smooth effects.

npx shadcn add "https://www.scrollxui.dev/registry/animated-textgenerate.json"
free

Visual style

glass-blur

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · medium
animationmotion

Files2 · source cached 10/9/2026, 11:06:15 PM

components/ui/animated-textgenerate.tsx
'use client';

import Link from 'next/link';
import { useEffect, useState } from 'react';
import { motion } from 'motion/react';
import { cn } from '@/lib/utils';

interface AnimatedTextGenerateProps {
  text: string;
  className?: string;
  textClassName?: string;
  blurEffect?: boolean;
  speed?: number;
  highlightWords?: string[];
  highlightClassName?: string;
  linkWords?: string[];
  linkHrefs?: string[];
  linkClassNames?: string[];
}

export const AnimatedTextGenerate = ({
  text,
  className,
  textClassName,
  blurEffect = true,
  speed = 0.5,
  highlightWords = [],
  highlightClassName,
  linkWords = [],
  linkHrefs = [],
  linkClassNames = [],
}: AnimatedTextGenerateProps) => {
  const [currentText, setCurrentText] = useState(text);
  const [visibleCount, setVisibleCount] = useState(0);
  const splitWords = text.split(' ');

  if (currentText !== text) {
    setCurrentText(text);
    setVisibleCount(0);
  }

  useEffect(() => {
    const intervalId = setInterval(
      () => {
        setVisibleCount((prev) => {
          if (prev >= splitWords.length) {
            clearInterval(intervalId);
            return prev;
          }
          return prev + 1;
        });
      },
      Math.max(speed * 200, 100),
    );

    return () => clearInterval(intervalId);
  }, [speed, splitWords.length]);

  const generateWords = () => {
    return (
      <div className='flex flex-wrap items-center gap-1'>
        {splitWords.map((word, idx) => {
          const isVisible = idx < visibleCount;
          const remaining = splitWords.length - visibleCount;
          let capsuleCount = 4;
          if (remaining <= 2) capsuleCount = remaining;
          else if (remaining <= 4) capsuleCount = Math.min(3, remaining);
          else if (visibleCount === 0) capsuleCount = 2;
          else if (visibleCount < 3) capsuleCount = 3;

          const isUpcoming =
            idx >= visibleCount && idx < visibleCount + capsuleCount;
          const isHighlight =
            highlightWords.length > 0 &&
            highlightWords.some((hw) =>
              word.toLowerCase().includes(hw.toLowerCase()),
            );
          const linkIndex = linkWords.findIndex((lw) =>
            word.toLowerCase().includes(lw.toLowerCase()),
          );
          const isLink = linkIndex !== -1;

          if (isVisible) {
            const wordElement = (
              <motion.span
                key={`${word}-${idx}`}
                initial={{
                  opacity: 0,
                  filter: blurEffect ? 'blur(10px)' : 'none',
                }}
                animate={{
                  opacity: 1,
                  filter: blurEffect ? 'blur(0px)' : 'none',
                }}
                transition={{
                  duration: speed * 0.3,
                  ease: 'easeOut',
                }}
                className={cn(
                  'dark:text-white text-black',
                  isHighlight && highlightClassName,
                )}
              >
                {word}
              </motion.span>
            );

            if (isLink && linkHrefs[linkIndex]) {
              return (
                <Link
                  href={linkHrefs[linkIndex]}
                  key={`link-${idx}`}
                  className={cn(linkClassNames[linkIndex])}
                >
                  {wordElement}
                </Link>
              );
            }
            return wordElement;
          }

          if (isUpcoming) {
            return (
              <motion.div
                key={`placeholder-${idx}`}
                initial={{ opacity: 0, scale: 0.8 }}
                animate={{ opacity: 0.4, scale: 1 }}
                exit={{ opacity: 0, scale: 0.8 }}
                transition={{ duration: 0.2 }}
                className='bg-black dark:bg-gray-600 rounded-full'
                style={{
                  width: `${Math.max(word.length * 0.7, 2.5)}em`,
                  height: '0.9em',
                  display: 'inline-block',
                }}
              />
            );
          }

          return null;
        })}
      </div>
    );
  };

  return (
    <div className={cn('font-bold', className)}>
      <div className='mt-4'>
        <div
          className={cn(
            'dark:text-white text-black text-2xl leading-snug tracking-wide',
            textClassName,
          )}
        >
          {generateWords()}
        </div>
      </div>
    </div>
  );
};
lib/utils.ts
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

More from ScrollX UI

Browse all components in this library →