Please use Desktop to view and interact with components
Components30mslinear
AI Response Typing Stream
Character-by-character typing animation with natural pauses and thinking states
aitypingstreamchatbotresponseanimationtext
Useto navigate between components
Preview
Ready
Code
TypeScript + React
'use client'
import { useState, useEffect, useRef } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
export function AIResponseTyping() {
const [displayedText, setDisplayedText] = useState('')
const [isTyping, setIsTyping] = useState(false)
const text = 'Hello! I\'m an AI assistant. I can help you with questions, provide information, and assist with various tasks. Feel free to ask me anything!'
const speed = 30
const showCursor = true
const intervalRef = useRef<NodeJS.Timeout | null>(null)
useEffect(() => {
if (!text) {
setIsTyping(false)
return
}
setIsTyping(true)
setDisplayedText('')
let currentIndex = 0
const chars = text.split('')
const typeNextChar = () => {
if (currentIndex < chars.length) {
const nextChar = chars[currentIndex]
const isPause =
nextChar === '.' ||
nextChar === ',' ||
nextChar === '!' ||
nextChar === '?' ||
nextChar === '\n'
setDisplayedText((prev) => prev + nextChar)
currentIndex++
if (isPause) {
intervalRef.current = setTimeout(() => {
typeNextChar()
}, speed * 3)
} else {
intervalRef.current = setTimeout(() => {
typeNextChar()
}, speed)
}
} else {
if (intervalRef.current) clearTimeout(intervalRef.current)
setIsTyping(false)
}
}
typeNextChar()
return () => {
if (intervalRef.current) clearTimeout(intervalRef.current)
}
}, [])
return (
<div className="w-full max-w-2xl">
<div className="relative rounded-2xl border border-border bg-card p-6 min-h-[100px]">
{displayedText && (
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-foreground leading-relaxed whitespace-pre-wrap break-words"
>
{displayedText}
{showCursor && isTyping && (
<motion.span
className="inline-block w-0.5 h-5 bg-primary ml-1 align-middle"
animate={{ opacity: [1, 1, 1, 0, 0] }}
transition={{
duration: 1,
repeat: Infinity,
repeatType: 'loop',
ease: 'linear',
}}
/>
)}
{showCursor && !isTyping && displayedText && (
<motion.span
className="inline-block w-0.5 h-5 bg-primary ml-1 align-middle opacity-50"
animate={{ opacity: [0.5, 0.5, 0, 0] }}
transition={{
duration: 1,
repeat: Infinity,
repeatType: 'loop',
ease: 'linear',
repeatDelay: 0.5,
}}
/>
)}
</motion.p>
)}
{isTyping && (
<motion.div
className="absolute inset-0 pointer-events-none overflow-hidden rounded-2xl"
initial={{ x: '-100%' }}
animate={{ x: '100%' }}
transition={{
duration: 2,
repeat: Infinity,
ease: 'linear',
}}
style={{
background: 'linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent)',
}}
/>
)}
{isTyping && (
<div className="absolute inset-0 rounded-2xl pointer-events-none overflow-hidden">
<motion.div
className="absolute inset-0 rounded-2xl"
animate={{
background: [
'linear-gradient(135deg, transparent, transparent)',
'linear-gradient(135deg, rgba(139, 92, 246, 0.1), transparent)',
'linear-gradient(135deg, transparent, transparent)',
],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: 'easeInOut',
}}
/>
</div>
)}
</div>
<div className="mt-3 flex items-center gap-2">
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<motion.div
animate={{
scale: isTyping ? [1, 1.2, 1] : 1,
}}
transition={{
duration: 0.5,
repeat: isTyping ? Infinity : 0,
ease: 'easeInOut',
}}
className="w-2 h-2 rounded-full bg-green-500"
/>
<span>{isTyping ? 'AI is typing...' : 'Ready'}</span>
</div>
</div>
</div>
)
}
How to Use
- 1Install Framer Motion:
npm install framer-motion - 2Copy the code from above
- 3Paste it into your project and customize as needed
- 4Colors are customizable via Tailwind CSS classes. The default theme uses dark mode colors defined in your globals.css file