This guide will have you running a fully functional chat application with real-time messaging, media sharing, and WhatsApp-like UI in just a few minutes.
Prerequisites
Before you begin, ensure you have:
Node.js 20+ installed on your system
npm , yarn , or pnpm package manager
A code editor (VS Code recommended)
Installation
Install dependencies
Choose your preferred package manager: This installs all required packages including Next.js, React, Zustand, Tailwind CSS, and UI components.
Start the development server
Launch the application in development mode: The dev server will start with Turbopack for fast refresh.
Open the application
Navigate to http://localhost:3000 in your browser. You’ll see the chat interface with demo conversations already loaded.
The application uses mock data for demonstration. You’ll see several pre-populated conversations that you can interact with immediately.
Your first interaction
Let’s explore the core features:
Send a message
Select a conversation
Click any contact in the left sidebar to open their chat.
Type your message
Click the message input at the bottom and type your text. Press Enter to send (or Shift+Enter for a new line). // The message is created with initial status
const message : Message = {
id: generateMessageId (),
chatId: activeChatId ,
authorId: profile . id ,
contentType: "text" ,
text: "Hello from the quickstart!" ,
status: "queued" ,
createdAt: new Date (). toISOString ()
}
Watch the status updates
Your message will automatically progress through delivery states:
⏱️ Queued → ⏰ Sending → ✓ Sent → ✓✓ Delivered → ✓✓ Read (blue)
Each transition happens with a ~450ms delay, simulating real network conditions.
Share an image
Open the file picker
Click the paperclip icon (📎) in the message composer, or drag and drop an image directly onto the input area.
Preview and send
You’ll see a thumbnail preview with a remove button (✕). Click the send button to share the image. // Media attachments are created with preview URLs
const media : MediaAttachment = {
id: `upload- ${ file . name } - ${ file . lastModified } ` ,
type: "image" ,
url: URL . createObjectURL ( file ),
localObjectUrl: objectUrl ,
sizeInBytes: file . size
}
View in conversation
Images appear in message bubbles with rounded corners and hover effects. Click any image to preview it full-size.
Use the emoji picker
Open emoji palette
Click the smiley face icon (😊) to open the emoji picker powered by emoji-mart.
Select an emoji
Browse categories or search for emojis. Click to insert at the cursor position. // Emojis are inserted as native Unicode characters
setText (( prev ) => ` ${ prev }${ emoji . native } ` )
Understanding the code
Main application structure
The entry point renders the ChatApp component in app/page.tsx:
import { ChatApp } from "@/components/chat/chat-app" ;
export default function Home () {
return (
< main className = "flex min-h-screen items-stretch bg-app-surface" >
< ChatApp />
</ main >
);
}
State management with Zustand
All chat data is managed through a centralized Zustand store in lib/chat/state.ts:
export const useChatStore = create < ChatStore >()( persist (
( set , get ) => ({
chats: {},
messages: {},
contacts: {},
drafts: {},
typingIndicators: [],
activeChatId: undefined ,
// Send a message and trigger status lifecycle
sendComposerPayload : ( chatId , payload ) => {
const message = createMessage ( chatId , payload )
set (( state ) => ({
messages: { ... state . messages , [message.id]: message },
chats: {
... state . chats ,
[chatId]: updateChatWithMessage ( state . chats [ chatId ], message )
}
}))
scheduleLifecycle ( message . id , get , set )
return message . id
},
// Receive incoming messages
receiveMessage : ( chatId , message ) => {
set (( state ) => ({
messages: { ... state . messages , [message.id]: message },
chats: {
... state . chats ,
[chatId]: {
... state . chats [ chatId ],
unreadCount: state . activeChatId === chatId
? 0
: state . chats [ chatId ]. unreadCount + 1
}
}
}))
}
}),
{
name: 'chat-store' ,
storage: createJSONStorage (() => localStorage )
}
))
Chat simulator for demo
The useChatSimulator hook in hooks/use-chat-simulator.ts simulates incoming messages:
hooks/use-chat-simulator.ts
export function useChatSimulator () {
const contacts = useChatStore (( state ) => state . contacts )
const receiveMessage = useChatStore (( state ) => state . receiveMessage )
const setTypingIndicator = useChatStore (( state ) => state . setTypingIndicator )
useEffect (() => {
// Send simulated messages every 18 seconds
const interval = setInterval (() => {
const chat = pickRandomChat ()
const contact = contacts [ chat . contactId ]
// Show typing indicator
setTypingIndicator ( chat . id , contact . id , true )
// Wait 2-4 seconds, then send message
setTimeout (() => {
setTypingIndicator ( chat . id , contact . id , false )
const message = createInboundTextMessage ({
chatId: chat . id ,
authorId: contact . id ,
text: pickRandomResponse ()
})
receiveMessage ( chat . id , message )
}, 2200 + Math . random () * 1800 )
}, 18000 )
return () => clearInterval ( interval )
}, [ contacts , receiveMessage , setTypingIndicator ])
}
Message bubble component
Individual messages are rendered by MessageBubble in components/chat/message-bubble.tsx:
components/chat/message-bubble.tsx
export const MessageBubble = memo (({
message ,
isOutgoing ,
showAvatar ,
onMediaPreview
} : MessageBubbleProps ) => {
const StatusIcon = statusIcon [ message . status ]
const timeLabel = format ( new Date ( message . createdAt ), "HH:mm" )
return (
< div className = { cn (
"flex gap-2" ,
isOutgoing ? "justify-end" : "justify-start"
)} >
{! isOutgoing && showAvatar && (
< div className = "h-7 w-7 rounded-full bg-secondary" >
{ contactInitials }
</ div >
)}
< div className = { cn (
"rounded-3xl px-4 py-2 shadow-sm" ,
isOutgoing
? "rounded-br-lg bg-bubble-outgoing"
: "rounded-bl-lg bg-bubble-incoming"
)} >
{ message . contentType === " image " && message . media && (
< Image
src = {message.media. url }
alt = {message.media.caption ?? "Shared media" }
width = {message.media.width ?? 640 }
height = {message.media.height ?? 360 }
className = "rounded-2xl"
/>
)}
{ message . text && (
< p className = "text-sm leading-snug" > {message. text } </ p >
)}
< footer className = "mt-1 flex items-center gap-1 text-xs" >
< span >{ timeLabel } </ span >
{ isOutgoing && (
< StatusIcon className = {statusColor [message.status]} />
)}
</footer>
</div>
</div>
)
})
Next steps
Now that you have the app running, explore these topics:
Installation Learn about the full dependency tree and package configuration
State management Deep dive into the Zustand store structure and persistence
Component library Explore the Radix UI components and Tailwind styling
Message lifecycle Understand how messages transition through delivery states
Pro tip : Check the browser’s localStorage to see persisted chat data. Open DevTools → Application → Local Storage → chat-store