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

TypeScript - components/MessageBubble.tsx
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

TypeScript - components/TypingIndicator.tsx
export function TypingIndicator() {
  return (
    <div className="typing-indicator">
      <span /><span /><span />
    </div>
  );
}

Chat Input with Submit

TypeScript - components/ChatInput.tsx
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:

TypeScript - components/Chat.tsx
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>
  );
}
Accessibility: Always include 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.