Skip to main content
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:
app/globals.css
@import "tailwindcss";
@import "tw-animate-css";
package.json
{
  "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.

Button hover transitions

components/ui/button.tsx
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>

Animation performance

Hardware acceleration

Use transform and opacity for GPU-accelerated animations:
<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.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !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.

Build docs developers (and LLMs) love