Expert knowledge for creative text animations - typewriter effects, character-by-character reveals, kinetic typography, split text animations, and expressive text motion.
From react-animation-studionpx claudepluginhub markus41/claude --plugin react-animation-studioThis skill uses the workspace's default tool permissions.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Migrates code, prompts, and API calls from Claude Sonnet 4.0/4.5 or Opus 4.1 to Opus 4.5, updating model strings on Anthropic, AWS, GCP, Azure platforms.
Configures VPN and dedicated connections like Direct Connect, ExpressRoute, Interconnect for secure on-premises to AWS, Azure, GCP, OCI hybrid networking.
Expert knowledge for creative text animations - typewriter effects, character-by-character reveals, kinetic typography, split text animations, and expressive text motion.
Activate this skill when:
**/*.tsx with text animation components**/components/Text*.tsx**/components/Heading*.tsx**/components/Title*.tsximport { 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>
);
}
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>
);
}
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>
);
}
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>
);
}
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>
);
}
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>
);
}
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>
);
}
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>;
}
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>
);
}
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>;
}
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>
);
}
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>
);
}
Created by Brookside BI as part of React Animation Studio