Building Chat Components Intermediate
A polished AI chat interface needs more than a text input and a message list. In this lesson, you will build reusable components for message bubbles, typing indicators, markdown rendering, code highlighting, and auto-scroll behavior.
Message Bubble Component
import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; interface MessageBubbleProps { role: 'user' | 'assistant'; content: string; } export function MessageBubble({ role, content }: MessageBubbleProps) { return ( <div className={`message-bubble ${role}`}> <div className="message-avatar"> {role === 'user' ? 'You' : 'AI'} </div> <div className="message-content"> <ReactMarkdown remarkPlugins={[remarkGfm]}> {content} </ReactMarkdown> </div> </div> ); }
Typing Indicator
export function TypingIndicator() { return ( <div className="typing-indicator"> <span /><span /><span /> </div> ); }
Chat Input with Submit
import { FormEvent, KeyboardEvent } from 'react'; interface ChatInputProps { input: string; onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void; onSubmit: (e: FormEvent) => void; isLoading: boolean; } export function ChatInput({ input, onChange, onSubmit, isLoading }: ChatInputProps) { function handleKeyDown(e: KeyboardEvent) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); onSubmit(e as unknown as FormEvent); } } return ( <form onSubmit={onSubmit} className="chat-input-form"> <textarea value={input} onChange={onChange} onKeyDown={handleKeyDown} placeholder="Type a message... (Enter to send, Shift+Enter for new line)" disabled={isLoading} rows={1} /> <button type="submit" disabled={isLoading || !input.trim()}> {isLoading ? '...' : 'Send'} </button> </form> ); }
Complete Chat Component
Combine all components into a complete, auto-scrolling chat interface:
import { useRef, useEffect } from 'react'; import { useChat } from '@ai-sdk/react'; import { MessageBubble } from './MessageBubble'; import { ChatInput } from './ChatInput'; import { TypingIndicator } from './TypingIndicator'; export function Chat() { const bottomRef = useRef<HTMLDivElement>(null); const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({ api: '/api/chat' }); useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); return ( <div className="chat-container"> <div className="messages-list"> {messages.map(m => ( <MessageBubble key={m.id} role={m.role} content={m.content} /> ))} {isLoading && <TypingIndicator />} <div ref={bottomRef} /> </div> <ChatInput input={input} onChange={handleInputChange} onSubmit={handleSubmit} isLoading={isLoading} /> </div> ); }
aria-label attributes on interactive elements, use semantic HTML, and ensure keyboard navigation works. Screen readers should be able to announce new messages as they arrive.
Components Built!
You have a polished set of chat components. In the next lesson, you will dive deep into streaming - how it works and how to optimize it.
Next: Streaming →Ready to Go Deeper?
Live instructor-led courses from our partners. Affiliate disclosure.
AI & ML Courses - 30% Off
Live instructor-led AI, machine learning, data science, and cloud courses for working professionals. Use code Limited30 at checkout.
EdurekaDataCamp - AI & Data Science
Hands-on Python, machine learning, and AI courses with interactive exercises and real projects.
DataCampedX - Top AI Courses
University-level AI courses from MIT, Harvard, Stanford. Earn certificates that employers recognize.
edX