Implementation of Text Animation (Typed.js, Split Text) on Website
Text animations fall into two classes: typewriter effect (Typed.js, TypeIt) and character/word-by-word animations through text splitting (GSAP SplitText, anime.js, or manual implementation). The first class creates the illusion of real-time input. The second allows animating each letter or word independently — wave effects, stagger, 3D transforms.
Typed.js: Typewriter Effect
npm install typed.js
// components/TypedText.tsx
import { useEffect, useRef } from 'react'
import Typed from 'typed.js'
interface TypedTextProps {
strings: string[]
typeSpeed?: number // ms per character
backSpeed?: number // ms per character deletion
backDelay?: number // delay before deletion
loop?: boolean
showCursor?: boolean
cursorChar?: string
onComplete?: () => void
}
export function TypedText({
strings,
typeSpeed = 60,
backSpeed = 30,
backDelay = 1500,
loop = true,
showCursor = true,
cursorChar = '|',
onComplete,
}: TypedTextProps) {
const elementRef = useRef<HTMLSpanElement>(null)
const typedRef = useRef<Typed | null>(null)
useEffect(() => {
if (!elementRef.current) return
typedRef.current = new Typed(elementRef.current, {
strings,
typeSpeed,
backSpeed,
backDelay,
loop,
showCursor,
cursorChar,
onComplete: (self) => {
onComplete?.()
},
// HTML tags in strings will be applied as markup
contentType: 'html',
})
return () => {
typedRef.current?.destroy()
}
}, []) // eslint-disable-line react-hooks/exhaustive-deps
return (
<span>
<span ref={elementRef} />
</span>
)
}
// Usage
<h1 className="text-5xl font-bold">
We create{' '}
<TypedText
strings={[
'<span class="text-blue-500">websites</span>',
'<span class="text-purple-500">applications</span>',
'<span class="text-pink-500">products</span>',
]}
typeSpeed={70}
backSpeed={40}
loop
/>
</h1>
Custom Implementation Without Library
For simple cases — custom typewriter without dependencies:
// hooks/useTypewriter.ts
import { useState, useEffect, useRef } from 'react'
interface TypewriterOptions {
strings: string[]
typeSpeed?: number
deleteSpeed?: number
pauseMs?: number
loop?: boolean
}
export function useTypewriter({
strings,
typeSpeed = 60,
deleteSpeed = 30,
pauseMs = 2000,
loop = true,
}: TypewriterOptions) {
const [displayText, setDisplayText] = useState('')
const [isTyping, setIsTyping] = useState(true)
const indexRef = useRef(0)
const charRef = useRef(0)
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
useEffect(() => {
const tick = () => {
const current = strings[indexRef.current]
if (isTyping) {
if (charRef.current < current.length) {
setDisplayText(current.slice(0, charRef.current + 1))
charRef.current++
timerRef.current = setTimeout(tick, typeSpeed)
} else {
// Pause before deletion
timerRef.current = setTimeout(() => {
setIsTyping(false)
tick()
}, pauseMs)
}
} else {
if (charRef.current > 0) {
setDisplayText(current.slice(0, charRef.current - 1))
charRef.current--
timerRef.current = setTimeout(tick, deleteSpeed)
} else {
// Move to next string
indexRef.current = loop
? (indexRef.current + 1) % strings.length
: Math.min(indexRef.current + 1, strings.length - 1)
setIsTyping(true)
timerRef.current = setTimeout(tick, typeSpeed)
}
}
}
timerRef.current = setTimeout(tick, typeSpeed)
return () => {
if (timerRef.current) clearTimeout(timerRef.current)
}
}, [strings, typeSpeed, deleteSpeed, pauseMs, loop, isTyping])
return { displayText, isTyping }
}
GSAP SplitText: Character-by-Character Animations
SplitText (paid GSAP plugin) splits text into <div> by characters, words or lines:
// components/SplitTextReveal.tsx
import { useEffect, useRef } from 'react'
import { gsap } from 'gsap'
import { SplitText } from 'gsap/SplitText'
gsap.registerPlugin(SplitText)
interface SplitTextRevealProps {
text: string
type?: 'chars' | 'words' | 'lines'
stagger?: number
className?: string
}
export function SplitTextReveal({
text,
type = 'chars',
stagger = 0.03,
className = '',
}: SplitTextRevealProps) {
const containerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (!containerRef.current) return
const ctx = gsap.context(() => {
const split = new SplitText(containerRef.current!, {
type,
linesClass: 'split-line',
wordsClass: 'split-word',
charsClass: 'split-char',
})
const elements =
type === 'chars'
? split.chars
: type === 'words'
? split.words
: split.lines
gsap.from(elements, {
y: '120%',
opacity: 0,
rotationX: -60,
transformOrigin: '0% 50% -50',
ease: 'back.out(1.5)',
duration: 0.6,
stagger,
scrollTrigger: {
trigger: containerRef.current,
start: 'top 80%',
once: true,
},
})
}, containerRef)
return () => ctx.revert()
}, [type, stagger])
return (
<div
ref={containerRef}
className={`overflow-hidden ${className}`}
style={{ perspective: '600px' }}
>
{text}
</div>
)
}
Manual SplitText Implementation Without Paid Plugin
// utils/split-text.ts
export function splitIntoSpans(
element: HTMLElement,
mode: 'chars' | 'words'
): HTMLElement[] {
const text = element.textContent ?? ''
const spans: HTMLElement[] = []
element.innerHTML = ''
const parts = mode === 'chars' ? text.split('') : text.split(/\s+/)
parts.forEach((part, i) => {
const span = document.createElement('span')
span.textContent = mode === 'words' ? part : (part === ' ' ? '\u00A0' : part)
span.style.display = 'inline-block'
span.style.overflow = 'hidden'
element.appendChild(span)
if (mode === 'words' && i < parts.length - 1) {
element.appendChild(document.createTextNode(' '))
}
spans.push(span)
})
return spans
}
// hooks/useTextReveal.ts
import { useEffect, useRef } from 'react'
import { splitIntoSpans } from '../utils/split-text'
export function useTextReveal(mode: 'chars' | 'words' = 'words') {
const ref = useRef<HTMLElement>(null)
useEffect(() => {
const el = ref.current
if (!el) return
const spans = splitIntoSpans(el, mode)
const observer = new IntersectionObserver(
([entry]) => {
if (!entry.isIntersecting) return
spans.forEach((span, i) => {
span.animate(
[
{ opacity: 0, transform: 'translateY(100%)' },
{ opacity: 1, transform: 'translateY(0%)' },
],
{
duration: 500,
delay: i * (mode === 'chars' ? 30 : 80),
easing: 'cubic-bezier(0.16, 1, 0.3, 1)',
fill: 'forwards',
}
)
})
observer.unobserve(el)
},
{ threshold: 0.3 }
)
observer.observe(el)
return () => observer.disconnect()
}, [mode])
return ref
}
Glitch Effect for Headings
/* styles/glitch.css */
@keyframes glitch-1 {
0%, 100% { clip-path: inset(0 0 95% 0); transform: translate(-3px, 0); }
20% { clip-path: inset(20% 0 60% 0); transform: translate(3px, 0); }
40% { clip-path: inset(50% 0 30% 0); transform: translate(-2px, 0); }
60% { clip-path: inset(80% 0 5% 0); transform: translate(2px, 0); }
}
@keyframes glitch-2 {
0%, 100% { clip-path: inset(50% 0 30% 0); transform: translate(3px, 0); color: #ff0070; }
30% { clip-path: inset(10% 0 70% 0); transform: translate(-3px, 0); }
70% { clip-path: inset(80% 0 5% 0); transform: translate(2px, 0); color: #00d4ff; }
}
.glitch {
position: relative;
}
.glitch::before,
.glitch::after {
content: attr(data-text);
position: absolute;
inset: 0;
animation-duration: 0.8s;
animation-iteration-count: infinite;
animation-timing-function: steps(1);
}
.glitch::before { animation-name: glitch-1; }
.glitch::after { animation-name: glitch-2; }
Typical Timeframes
Typed.js effect for one heading — 2 hours. Character-by-character animations for multiple sections via Web Animations API — 1 business day. Full system with GSAP SplitText, wave effects, glitch and scroll-triggers — 2–3 business days.







