text-animations

v2026.09.25

Expert knowledge for creative text animations - typewriter effects, character-by-character reveals, kinetic typography, split text animations, and expressive text motion.

GitHub
Install command
npx skhub add thelobbi/text-animations
Markdown
SKILL.md

Text Animations Skill

Expert knowledge for creative text animations - typewriter effects, character-by-character reveals, kinetic typography, split text animations, and expressive text motion.

When to Use

Activate this skill when:

  • User wants typewriter/typing animations
  • Creating character-by-character text reveals
  • Building kinetic typography effects
  • Need animated headings or titles
  • Creating text scramble effects
  • Building split-text animations

File Patterns

  • **/*.tsx with text animation components
  • **/components/Text*.tsx
  • **/components/Heading*.tsx
  • **/components/Title*.tsx

Text Animation Types

1. Typewriter Effects

Basic Typewriter

import { motion, useAnimation } from 'framer-motion';
import { useEffect, useState } from 'react';

export function Typewriter({
  text,
  speed = 50,
  delay = 0,
}: {
  text: string;
  speed?: number;
  delay?: number;
}) {
  const [displayText, setDisplayText] = useState('');
  const [isComplete, setIsComplete] = useState(false);

  useEffect(() => {
    const timeout = setTimeout(() => {
      let i = 0;
      const interval = setInterval(() => {
        if (i < text.length) {
          setDisplayText(text.slice(0, i + 1));
          i++;
        } else {
          setIsComplete(true);
          clearInterval(interval);
        }
      }, speed);

      return () => clearInterval(interval);
    }, delay);

    return () => clearTimeout(timeout);
  }, [text, speed, delay]);

  return (
    <span>
      {displayText}
      {!isComplete && (
        <motion.span
          animate={{ opacity: [1, 0] }}
          transition={{ duration: 0.5, repeat: Infinity }}
        >
          |
        </motion.span>
      )}
    </span>
  );
}

Typewriter with Delete

export function TypewriterLoop({
  texts,
  typingSpeed = 50,
  deletingSpeed = 30,
  pauseDuration = 2000,
}: {
  texts: string[];
  typingSpeed?: number;
  deletingSpeed?: number;
  pauseDuration?: number;
}) {
  const [currentTextIndex, setCurrentTextIndex] = useState(0);
  const [displayText, setDisplayText] = useState('');
  const [isDeleting, setIsDeleting] = useState(false);

  useEffect(() => {
    const currentFullText = texts[currentTextIndex];

    const timeout = setTimeout(() => {
      if (!isDeleting) {
        if (displayText.length < currentFullText.length) {
          setDisplayText(currentFullText.slice(0, displayText.length + 1));
        } else {
          setTimeout(() => setIsDeleting(true), pauseDuration);
        }
      } else {
        if (displayText.length > 0) {
          setDisplayText(displayText.slice(0, -1));
        } else {
          setIsDeleting(false);
          setCurrentTextIndex((prev) => (prev + 1) % texts.length);
        }
      }
    }, isDeleting ? deletingSpeed : typingSpeed);

    return () => clearTimeout(timeout);
  }, [displayText, isDeleting, currentTextIndex, texts, typingSpeed, deletingSpeed, pauseDuration]);

  return (
    <span>
      {displayText}
      <motion.span
        className="inline-block w-0.5 h-[1em] bg-current ml-1"
        animate={{ opacity: [1, 0] }}
        transition={{ duration: 0.5, repeat: Infinity }}
      />
    </span>
  );
}

2. Character-by-Character Animations

Staggered Characters

export function StaggeredText({
  text,
  staggerDelay = 0.03,
}: {
  text: string;
  staggerDelay?: number;
}) {
  const characters = text.split('');

  const containerVariants = {
    hidden: {},
    visible: {
      transition: { staggerChildren: staggerDelay },
    },
  };

  const charVariants = {
    hidden: { opacity: 0, y: 20 },
    visible: {
      opacity: 1,
      y: 0,
      transition: { type: 'spring', stiffness: 300, damping: 20 },
    },
  };

  return (
    <motion.span
      variants={containerVariants}
      initial="hidden"
      animate="visible"
      className="inline-block"
    >
      {characters.map((char, i) => (
        <motion.span
          key={i}
          variants={charVariants}
          className="inline-block"
          style={{ whiteSpace: char === ' ' ? 'pre' : 'normal' }}
        >
          {char}
        </motion.span>
      ))}
    </motion.span>
  );
}

Wave Text

export function WaveText({ text }: { text: string }) {
  const characters = text.split('');

  return (
    <span className="inline-block">
      {characters.map((char, i) => (
        <motion.span
          key={i}
          className="inline-block"
          animate={{
            y: [0, -10, 0],
          }}
          transition={{
            duration: 0.6,
            delay: i * 0.05,
            repeat: Infinity,
            repeatDelay: 1,
          }}
          style={{ whiteSpace: char === ' ' ? 'pre' : 'normal' }}
        >
          {char}
        </motion.span>
      ))}
    </span>
  );
}

Bouncy Text

export function BouncyText({ text }: { text: string }) {
  const characters = text.split('');

  return (
    <span className="inline-block">
      {characters.map((char, i) => (
        <motion.span
          key={i}
          className="inline-block"
          whileHover={{
            y: -10,
            color: '#667eea',
            transition: {
              type: 'spring',
              stiffness: 500,
              damping: 10,
            },
          }}
          style={{ whiteSpace: char === ' ' ? 'pre' : 'normal' }}
        >
          {char}
        </motion.span>
      ))}
    </span>
  );
}

3. Word-by-Word Animations

Staggered Words

export function StaggeredWords({
  text,
  staggerDelay = 0.1,
}: {
  text: string;
  staggerDelay?: number;
}) {
  const words = text.split(' ');

  const containerVariants = {
    hidden: {},
    visible: {
      transition: { staggerChildren: staggerDelay },
    },
  };

  const wordVariants = {
    hidden: { opacity: 0, y: 20, filter: 'blur(10px)' },
    visible: {
      opacity: 1,
      y: 0,
      filter: 'blur(0px)',
      transition: { duration: 0.4, ease: 'easeOut' },
    },
  };

  return (
    <motion.span
      variants={containerVariants}
      initial="hidden"
      animate="visible"
      className="inline"
    >
      {words.map((word, i) => (
        <motion.span
          key={i}
          variants={wordVariants}
          className="inline-block mr-2"
        >
          {word}
        </motion.span>
      ))}
    </motion.span>
  );
}

Highlighted Words

export function HighlightedText({
  text,
  highlightWords,
  highlightColor = '#667eea',
}: {
  text: string;
  highlightWords: string[];
  highlightColor?: string;
}) {
  const words = text.split(' ');

  return (
    <span>
      {words.map((word, i) => {
        const isHighlighted = highlightWords.some(
          (hw) => word.toLowerCase().includes(hw.toLowerCase())
        );

        return (
          <motion.span
            key={i}
            className="inline-block mr-2"
            initial={{ opacity: 0, y: 10 }}
            animate={{ opacity: 1, y: 0 }}
            transition={{ delay: i * 0.05 }}
          >
            {isHighlighted ? (
              <motion.span
                className="relative inline-block"
                whileHover={{ scale: 1.05 }}
              >
                <motion.span
                  className="absolute inset-0 -z-10 rounded"
                  style={{ backgroundColor: highlightColor }}
                  initial={{ scaleX: 0 }}
                  animate={{ scaleX: 1 }}
                  transition={{ delay: i * 0.05 + 0.2, duration: 0.3 }}
                />
                <span className="relative text-white px-1">{word}</span>
              </motion.span>
            ) : (
              word
            )}
          </motion.span>
        );
      })}
    </span>
  );
}

4. Text Scramble Effects

Scramble Text

export function ScrambleText({
  text,
  duration = 1000,
}: {
  text: string;
  duration?: number;
}) {
  const [displayText, setDisplayText] = useState(text);
  const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*';

  useEffect(() => {
    let frame = 0;
    const totalFrames = 20;
    const interval = duration / totalFrames;

    const animate = setInterval(() => {
      frame++;

      const progress = frame / totalFrames;
      const revealedLength = Math.floor(progress * text.length);

      let result = '';
      for (let i = 0; i < text.length; i++) {
        if (i < revealedLength) {
          result += text[i];
        } else if (text[i] === ' ') {
          result += ' ';
        } else {
          result += characters[Math.floor(Math.random() * characters.length)];
        }
      }

      setDisplayText(result);

      if (frame >= totalFrames) {
        clearInterval(animate);
        setDisplayText(text);
      }
    }, interval);

    return () => clearInterval(animate);
  }, [text, duration]);

  return <span className="font-mono">{displayText}</span>;
}

5. Split Text / Line Animations

Split Lines

export function SplitLines({ text }: { text: string }) {
  const lines = text.split('\n');

  return (
    <div>
      {lines.map((line, i) => (
        <div key={i} className="overflow-hidden">
          <motion.div
            initial={{ y: '100%' }}
            animate={{ y: '0%' }}
            transition={{
              duration: 0.6,
              delay: i * 0.1,
              ease: [0.25, 0.1, 0.25, 1],
            }}
          >
            {line}
          </motion.div>
        </div>
      ))}
    </div>
  );
}

6. Counter/Number Animations

Counting Number

export function CountingNumber({
  from = 0,
  to,
  duration = 2,
  decimals = 0,
}: {
  from?: number;
  to: number;
  duration?: number;
  decimals?: number;
}) {
  const [count, setCount] = useState(from);

  useEffect(() => {
    const startTime = Date.now();
    const endTime = startTime + duration * 1000;

    const animate = () => {
      const now = Date.now();
      const progress = Math.min((now - startTime) / (duration * 1000), 1);

      // Ease out cubic
      const eased = 1 - Math.pow(1 - progress, 3);
      const currentValue = from + (to - from) * eased;

      setCount(currentValue);

      if (progress < 1) {
        requestAnimationFrame(animate);
      }
    };

    requestAnimationFrame(animate);
  }, [from, to, duration]);

  return <span>{count.toFixed(decimals)}</span>;
}

7. Gradient Text Animation

Animated Gradient Text

export function GradientText({ children }: { children: React.ReactNode }) {
  return (
    <motion.span
      className="bg-clip-text text-transparent"
      style={{
        backgroundImage: 'linear-gradient(90deg, #667eea, #764ba2, #f093fb, #667eea)',
        backgroundSize: '300% 100%',
      }}
      animate={{
        backgroundPosition: ['0% 50%', '100% 50%', '0% 50%'],
      }}
      transition={{
        duration: 5,
        repeat: Infinity,
        ease: 'linear',
      }}
    >
      {children}
    </motion.span>
  );
}

8. Outline Text Animation

Animated Stroke Text

export function StrokeText({ text }: { text: string }) {
  return (
    <svg viewBox="0 0 800 100" className="w-full h-auto">
      <motion.text
        x="50%"
        y="50%"
        textAnchor="middle"
        dominantBaseline="middle"
        className="text-6xl font-bold"
        fill="transparent"
        stroke="currentColor"
        strokeWidth="2"
        initial={{ strokeDasharray: 1000, strokeDashoffset: 1000 }}
        animate={{ strokeDashoffset: 0 }}
        transition={{ duration: 3, ease: 'easeOut' }}
      >
        {text}
      </motion.text>
    </svg>
  );
}

Examples

<example> Context: User wants a typewriter effect for hero text user: "Create a typewriter animation for my headline" assistant: Using the Typewriter component: ```tsx <h1 className="text-5xl font-bold"> <Typewriter text="Welcome to the future" speed={80} /> </h1> ``` </example> <example> Context: User needs animated counter for statistics user: "Animate the number counting up from 0 to 1000" assistant: Using CountingNumber: ```tsx <span className="text-4xl font-bold"> <CountingNumber to={1000} duration={2} /> users </span> ``` </example> <example> Context: User wants gradient animated heading user: "Make the heading text have a flowing gradient" assistant: Using GradientText: ```tsx <h1 className="text-6xl font-bold"> <GradientText>Innovation Starts Here</GradientText> </h1> ``` </example>

Related Skills

  • creative-effects - Glitch and distortion
  • framer-motion - Core animation library
  • css-animations - CSS-based text effects

Author

Created by Brookside BI as part of React Animation Studio

Discovery
Tags

No tags published for this skill.

Version
Latest version metadata

Version

v2026.09.25

Published

Sep 25, 2026

Category

Uncategorized

License

MIT

Source path

plugins/react-animation-studio/skills/text-animations

Default branch

main

Latest commit

2f1269c

Tree SHA

629e050