The WhatsApp Chat application uses CSS transitions and animations to create a polished, responsive user experience. Animations are powered by Tailwind CSS utilities and the tw-animate-css library.
Animation library
The project includes tw-animate-css for additional animation utilities:
@import "tailwindcss" ;
@import "tw-animate-css" ;
{
"devDependencies" : {
"tw-animate-css" : "^1.4.0"
}
}
The tw-animate-css package provides Tailwind-compatible versions of Animate.css animations like animate-bounce, animate-fade, and more.
Typing indicator animation
The typing indicator uses staggered bounce animations to simulate activity:
components/chat/typing-indicator.tsx
const TypingIndicatorComponent = ({ label = "typing" } : TypingIndicatorProps ) => {
const dots = [ 0 , 1 , 2 ]
return (
< div
className = "inline-flex items-center gap-2 rounded-full bg-black/5 px-3 py-1 text-xs font-medium text-muted-foreground"
aria-live = "polite"
aria-label = { ` ${ label } indicator` }
>
< span className = "sr-only" > { label } </ span >
< div className = "flex items-center gap-1" >
{ dots . map (( dot ) => (
< span
key = { dot }
className = "block h-1.5 w-1.5 animate-bounce rounded-full bg-accent"
style = { { animationDelay: ` ${ dot * 0.12 } s` } }
/>
)) }
</ div >
</ div >
)
}
Key animation features:
Staggered timing : Each dot has a 120ms delay increment
Bounce animation : Uses Tailwind’s built-in animate-bounce
Accessible : Includes aria-live for screen readers
The style prop applies inline animation delays, which Tailwind can’t generate dynamically.
Transition utilities
Components use Tailwind’s transition utilities for smooth state changes:
Message bubble transitions
components/chat/message-bubble.tsx
const bubbleClasses = cn (
"relative rounded-3xl px-4 py-2 shadow-sm transition-colors" ,
isOutgoing
? "rounded-br-lg bg-bubble-outgoing text-foreground"
: "rounded-bl-lg bg-bubble-incoming text-foreground"
)
The transition-colors utility animates background color changes smoothly.
const buttonVariants = cva (
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50" ,
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90" ,
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50" ,
},
},
}
)
The transition-all utility animates all CSS property changes including background, color, and transform.
components/chat/sidebar.tsx
< button
className = { cn (
"flex w-full items-center gap-3 rounded-2xl px-4 py-3 text-left transition" ,
isActive
? "bg-sidebar-accent/70 text-sidebar-foreground shadow-sm"
: "hover:bg-sidebar-accent/40"
) }
>
{ /* Chat preview content */ }
</ button >
Image animations
Hover scale effect
Images in message bubbles scale on hover for interactive feedback:
components/chat/message-bubble.tsx
< button
type = "button"
onClick = { () => onMediaPreview ?.( message . media as MediaAttachment ) }
className = "group mb-2 block overflow-hidden rounded-2xl border border-border/40 text-left transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40"
>
< Image
src = { message . media . url }
alt = { message . media . caption ?? "Shared media" }
width = { message . media . width ?? 640 }
height = { message . media . height ?? 360 }
className = "h-auto max-h-[320px] w-full object-cover transition duration-500 group-hover:scale-[1.02]"
/>
</ button >
Breakdown:
Group hover : .group on parent enables group-hover: on children
Slow transition : duration-500 creates a smooth 500ms animation
Subtle scale : scale-[1.02] provides gentle zoom (2% increase)
The overflow-hidden class on the parent prevents the scaled image from exceeding button boundaries.
Message send animation
The send button uses custom transitions and disabled states:
components/chat/message-composer.tsx
< Button
size = "icon"
disabled = { ! canSend }
onClick = { handleSend }
className = "h-11 w-11 rounded-full bg-primary text-primary-foreground shadow-lg transition hover:bg-primary/90 disabled:cursor-not-allowed disabled:bg-muted"
>
< PaperPlaneTilt className = "h-5 w-5" weight = "fill" />
< span className = "sr-only" > Send message </ span >
</ Button >
State transitions:
Enabled : Full opacity, primary background, shadow
Hover : Slightly darker background (bg-primary/90)
Disabled : Muted colors, not-allowed cursor
Drag and drop animations
The message composer animates during file drag operations:
components/chat/message-composer.tsx
const { getRootProps , getInputProps , isDragActive } = useDropzone ({
onDrop ,
multiple: true ,
accept: { "image/*" : [] },
})
return (
< div
{ ... getRootProps () }
className = { cn (
"relative border-t border-border/70 bg-background/95 px-4 pb-4 pt-3 transition-colors" ,
isDragActive && "border-primary bg-primary/5"
) }
>
< input { ... getInputProps () } />
{ /* Composer content */ }
</ div >
)
When dragging files:
Border color changes to border-primary
Background tints with bg-primary/5
Transitions animate smoothly via transition-colors
Message list animations
The message list uses Virtuoso for smooth scrolling with follow behavior:
components/chat/message-list.tsx
< Virtuoso
className = "flex-1"
style = { { height: "100%" } }
data = { timeline }
{ ... ( virtuosoInitialIndex !== undefined
? { initialTopMostItemIndex: virtuosoInitialIndex }
: {}) }
followOutput = { "smooth" }
alignToBottom
itemContent = { ( index , item ) => {
// Render message or divider
} }
/>
Key animation features:
Smooth follow : followOutput="smooth" auto-scrolls to new messages
Bottom alignment : alignToBottom anchors view to bottom
Initial position : Jumps to last message on chat switch
Virtuoso’s followOutput="smooth" provides momentum-based scrolling that feels native to mobile and desktop.
Custom animation patterns
Staggered list animations
For animating lists, use staggered delays:
{ items . map (( item , index ) => (
< div
key = { item . id }
className = "animate-fade-in"
style = { { animationDelay: ` ${ index * 50 } ms` } }
>
{ item . content }
</ div >
))}
Fade in on mount
Create a fade-in effect using opacity transitions:
const [ isMounted , setIsMounted ] = useState ( false )
useEffect (() => {
setIsMounted ( true )
}, [])
return (
< div className = { cn (
"transition-opacity duration-300" ,
isMounted ? "opacity-100" : "opacity-0"
) } >
{ children }
</ div >
)
Slide in from bottom
Combine transform and opacity for slide animations:
< div className = { cn (
"transition-all duration-300" ,
isVisible
? "translate-y-0 opacity-100"
: "translate-y-4 opacity-0"
) } >
{ content }
</ div >
Hardware acceleration
Use transform and opacity for GPU-accelerated animations:
Good - GPU accelerated
Avoid - CPU rendering
< div className = "transition-transform hover:scale-105" />
< div className = "transition-opacity hover:opacity-80" />
Reduce motion
Respect user preferences with prefers-reduced-motion:
@media (prefers-reduced-motion: reduce) {
* ,
* ::before ,
* ::after {
animation-duration : 0.01 ms !important ;
animation-iteration-count : 1 !important ;
transition-duration : 0.01 ms !important ;
}
}
Timing functions
Tailwind provides easing functions for natural motion:
// Linear (constant speed)
< div className = "transition ease-linear" />
// Ease-in (starts slow)
< div className = "transition ease-in" />
// Ease-out (ends slow) - best for exits
< div className = "transition ease-out" />
// Ease-in-out (smooth start and end) - best for most transitions
< div className = "transition ease-in-out" />
For most UI transitions, ease-in-out or the default easing provides the most natural feel.
Duration utilities
Control animation speed with duration classes:
// Fast (150ms) - micro-interactions
< Button className = "transition duration-150 hover:bg-primary/90" />
// Medium (300ms) - default for most transitions
< div className = "transition duration-300" />
// Slow (500ms) - emphasis animations
< Image className = "transition duration-500 hover:scale-105" />
// Very slow (700ms) - dramatic effects
< div className = "transition duration-700 animate-fade-in" />
Backdrop effects
Combine blur with transitions for glassmorphism:
components/chat/sidebar.tsx
< aside className = "bg-sidebar/80 backdrop-blur-xl transition-all" >
{ /* Content */ }
</ aside >
components/chat/chat-header.tsx
< header className = "bg-background/80 backdrop-blur transition-colors" >
{ /* Content */ }
</ header >
Backdrop blur creates depth and hierarchy without harsh borders.