Facebook Pixel

Chat UI Components in React Building a Polished Chat Interface

Learn how to build the React components for a real-time chat interface chat list, chat window, message bubbles, typing indicator, and message input.

Chat UI Components in React

A polished chat UI is essential for a good user experience. This guide covers the key React components.

Component Structure

src/components/Chat/
├── ChatPage.jsx           # Main chat page
├── ChatList.jsx           # List of conversations
├── ChatWindow.jsx         # Active conversation
├── MessageBubble.jsx      # Individual message
├── MessageInput.jsx       # Input area
├── TypingIndicator.jsx    # Typing animation
└── ChatHeader.jsx         # Chat header with user info

ChatPage (Layout)

const ChatPage = () => { const [selectedChat, setSelectedChat] = useState(null); return ( <div className="chat-page"> <div className="chat-sidebar"> <ChatList onSelectChat={setSelectedChat} selectedChat={selectedChat} /> </div> <div className="chat-main"> {selectedChat ? ( <ChatWindow targetUserId={selectedChat} /> ) : ( <EmptyState /> )} </div> </div> ); };

ChatList

const ChatList = ({ onSelectChat, selectedChat }) => { const [conversations, setConversations] = useState([]); const [onlineUsers, setOnlineUsers] = useState(new Set()); useEffect(() => { fetchConversations(); socket.on("user:online", ({ userId }) => { setOnlineUsers((prev) => new Set(prev).add(userId)); }); socket.on("user:offline", ({ userId }) => { setOnlineUsers((prev) => { const next = new Set(prev); next.delete(userId); return next; }); }); socket.on("message:received", () => { fetchConversations(); // Refresh chat list }); return () => { socket.off("user:online"); socket.off("user:offline"); socket.off("message:received"); }; }, []); const fetchConversations = async () => { const res = await fetch("/api/chat", { credentials: "include" }); const data = await res.json(); setConversations(data.data); }; return ( <div className="chat-list"> {conversations.map((conv) => ( <div key={conv._id} className={`chat-item ${selectedChat === conv.otherUser._id ? "selected" : ""}`} onClick={() => onSelectChat(conv.otherUser._id)} > <div className="avatar-wrapper"> <img src={conv.otherUser.photoUrl} alt="" className="avatar" /> {onlineUsers.has(conv.otherUser._id) && ( <span className="status-dot online" /> )} </div> <div className="chat-info"> <h4>{conv.otherUser.firstName} {conv.otherUser.lastName}</h4> <p className="last-message">{conv.lastMessage?.text}</p> </div> {conv.unreadCount > 0 && ( <span className="unread-badge">{conv.unreadCount}</span> )} </div> ))} </div> ); };

MessageBubble

const MessageBubble = ({ message, isOwn }) => { const time = new Date(message.createdAt).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" }); return ( <div className={`message-bubble ${isOwn ? "own" : "other"}`}> <p className="message-text">{message.text}</p> <div className="message-meta"> <span className="message-time">{time}</span> {isOwn && message.read && ( <span className="read-receipt">✓✓</span> )} </div> </div> ); };

MessageInput

const MessageInput = ({ targetUserId, onSend }) => { const [text, setText] = useState(""); const [isSending, setIsSending] = useState(false); const typingTimeoutRef = useRef(null); const handleTextChange = (e) => { setText(e.target.value); // Typing indicator socket.emit("typing:start", { targetUserId }); if (typingTimeoutRef.current) clearTimeout(typingTimeoutRef.current); typingTimeoutRef.current = setTimeout(() => { socket.emit("typing:stop", { targetUserId }); }, 3000); }; const handleSend = () => { if (!text.trim() || isSending) return; setIsSending(true); // Stop typing if (typingTimeoutRef.current) clearTimeout(typingTimeoutRef.current); socket.emit("typing:stop", { targetUserId }); socket.emit("message:send", { targetUserId, text }, (response) => { setIsSending(false); if (response.success) { setText(""); onSend(response.message); } }); }; return ( <div className="message-input"> <input type="text" value={text} onChange={handleTextChange} onKeyPress={(e) => e.key === "Enter" && handleSend()} placeholder="Type a message..." disabled={isSending} /> <button onClick={handleSend} disabled={isSending || !text.trim()}> {isSending ? "Sending..." : "Send"} </button> </div> ); };

TypingIndicator

const TypingIndicator = ({ userName }) => { return ( <div className="typing-indicator"> <div className="typing-dots"> <span></span> <span></span> <span></span> </div> <p>{userName} is typing...</p> </div> ); };

CSS Styling

.message-bubble.own { background: #fd267d; color: white; align-self: flex-end; border-radius: 20px 20px 0 20px; } .message-bubble.other { background: #f0f0f0; color: #333; align-self: flex-start; border-radius: 20px 20px 20px 0; } .typing-dots span { animation: typing 1.4s infinite; } .typing-dots span:nth-child(2) { animation-delay: 0.2s; } .typing-dots span:nth-child(3) { animation-delay: 0.4s; } @keyframes typing { 0%, 60%, 100% { opacity: 0.3; } 30% { opacity: 1; } }

The Takeaway

A polished chat UI includes: ChatPage (layout with sidebar and main), ChatList (conversations with online status and unread badges), ChatWindow (active conversation with messages), MessageBubble (own vs other styling with read receipts), MessageInput (with typing indicators and send button), and TypingIndicator (animated dots). Use CSS for visual polish own messages on the right with primary color, others on the left with gray background, and animated typing dots.

ChatPage (layout), ChatList (conversation list with online status and unread badges), ChatWindow (active conversation), MessageBubble (individual message with own/other styling), MessageInput (text input with typing indicator and send button), TypingIndicator (animated dots), and ChatHeader (user info and online status).

Own messages: background primary color, align right, border-radius 20px 20px 0 20px. Other messages: background gray, align left, border-radius 20px 20px 20px 0. Include timestamp and read receipt (double checkmark) for own messages.

Store unreadCount in the conversation. In ChatList, show a badge with the count if unreadCount > 0. When a chat is opened, emit messages:read to mark as read and reset the count. Listen for message:received to refresh the chat list and update unread counts.

Create three spans with CSS animation that changes opacity (0.3 → 1 → 0.3) with staggered delays (0s, 0.2s, 0.4s). Show the component when isTyping is true (from Socket.io typing:update event). Hide it when typing:stop is received or after a timeout.

Use a flex layout: ChatPage with a sidebar (ChatList) and main area (ChatWindow or empty state). The sidebar shows all conversations, the main area shows the selected chat. On mobile, show either the list or the chat (not both) with a back button.

Ready to master Node.js completely?

Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course to dive deeper with high-quality video tutorials, solve interview questions, and a premium community.

Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.