---
title: "Building a Chat UI with React: Message Bubbles, Input, and Auto-Scroll"
description: "Learn how to build a production-quality chat interface for AI agents using React and TypeScript. Covers message bubble components, input handling, and smooth auto-scroll behavior."
canonical: https://callsphere.ai/blog/building-chat-ui-react-message-bubbles-input-auto-scroll
category: "Learn Agentic AI"
tags: ["React", "Chat UI", "TypeScript", "Frontend", "AI Agent Interface"]
author: "CallSphere Team"
published: 2026-03-17T00:00:00.000Z
updated: 2026-05-06T01:02:44.935Z
---

# Building a Chat UI with React: Message Bubbles, Input, and Auto-Scroll

> Learn how to build a production-quality chat interface for AI agents using React and TypeScript. Covers message bubble components, input handling, and smooth auto-scroll behavior.

## Why Chat Is the Default Agent Interface

The chat paradigm dominates AI agent interfaces for good reason. Users already understand turn-based conversation from messaging apps, so adopting it for agent interaction eliminates onboarding friction. Building a solid chat UI in React requires three core components: a message list that renders bubbles, an input area that handles submissions, and auto-scroll logic that keeps the latest message visible without disrupting manual scrolling.

## Defining the Message Model

Start with a TypeScript type that represents a single chat message. This type drives rendering decisions throughout the component tree.

```mermaid
flowchart LR
    INPUT(["User intent"])
    PARSE["Parse plus
classify"]
    PLAN["Plan and tool
selection"]
    AGENT["Agent loop
LLM plus tools"]
    GUARD{"Guardrails
and policy"}
    EXEC["Execute and
verify result"]
    OBS[("Trace and metrics")]
    OUT(["Outcome plus
next action"])
    INPUT --> PARSE --> PLAN --> AGENT --> GUARD
    GUARD -->|Pass| EXEC --> OUT
    GUARD -->|Fail| AGENT
    AGENT --> OBS
    style AGENT fill:#4f46e5,stroke:#4338ca,color:#fff
    style GUARD fill:#f59e0b,stroke:#d97706,color:#1f2937
    style OBS fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
    style OUT fill:#059669,stroke:#047857,color:#fff
```

```typescript
interface ChatMessage {
  id: string;
  role: "user" | "assistant" | "system";
  content: string;
  timestamp: Date;
  status: "sending" | "sent" | "error";
}
```

The `role` field determines bubble alignment and styling. The `status` field enables optimistic UI patterns where messages appear immediately before server confirmation.

## The Message Bubble Component

Each message renders as a bubble with alignment and color based on the sender role.

```typescript
interface BubbleProps {
  message: ChatMessage;
}

function MessageBubble({ message }: BubbleProps) {
  const isUser = message.role === "user";

  return (

{message.content}

          {message.timestamp.toLocaleTimeString([], {
            hour: "2-digit",
            minute: "2-digit",
          })}

  );
}
```

Key design choices: `max-w-[75%]` prevents bubbles from stretching across the full viewport. The `rounded-br-md` and `rounded-bl-md` classes create a flat corner on the side where the bubble attaches to the sender, which is a familiar pattern from iMessage and WhatsApp.

## Auto-Scroll with Manual Override

Auto-scroll must bring new messages into view but stop scrolling when the user has intentionally scrolled up to read history. This requires tracking whether the user is near the bottom.

```typescript
import { useRef, useEffect, useCallback, useState } from "react";

function useAutoScroll(messages: ChatMessage[]) {
  const containerRef = useRef(null);
  const [isNearBottom, setIsNearBottom] = useState(true);

  const handleScroll = useCallback(() => {
    const el = containerRef.current;
    if (!el) return;
    const threshold = 100;
    const distanceFromBottom =
      el.scrollHeight - el.scrollTop - el.clientHeight;
    setIsNearBottom(distanceFromBottom  {
    if (isNearBottom && containerRef.current) {
      containerRef.current.scrollTo({
        top: containerRef.current.scrollHeight,
        behavior: "smooth",
      });
    }
  }, [messages, isNearBottom]);

  return { containerRef, handleScroll, isNearBottom };
}
```

The 100-pixel threshold prevents minor floating-point differences from breaking the near-bottom check. The `behavior: "smooth"` creates a polished animation instead of a jarring jump.

## The Chat Input Component

The input component handles both text entry and submission. It should support multi-line input with Shift+Enter and submit on Enter.

```typescript
import { useState, KeyboardEvent } from "react";

interface ChatInputProps {
  onSend: (text: string) => void;
  disabled?: boolean;
}

function ChatInput({ onSend, disabled }: ChatInputProps) {
  const [text, setText] = useState("");

  const handleKeyDown = (e: KeyboardEvent) => {
    if (e.key === "Enter" && !e.shiftKey) {
      e.preventDefault();
      if (text.trim()) {
        onSend(text.trim());
        setText("");
      }
    }
  };

  return (

       setText(e.target.value)}
        onKeyDown={handleKeyDown}
        placeholder="Type a message..."
        disabled={disabled}
        rows={1}
        className="flex-1 resize-none rounded-xl border px-4 py-2.5
                   focus:outline-none focus:ring-2 focus:ring-blue-500"
      />
       {
          if (text.trim()) {
            onSend(text.trim());
            setText("");
          }
        }}
        disabled={disabled || !text.trim()}
        className="rounded-xl bg-blue-600 px-4 py-2.5 text-white
                   disabled:opacity-50"
      >
        Send

  );
}
```

## Assembling the Full Chat Container

Combine the bubble list, auto-scroll hook, and input into a single container component.

```typescript
function AgentChat() {
  const [messages, setMessages] = useState([]);
  const { containerRef, handleScroll } = useAutoScroll(messages);

  const sendMessage = async (text: string) => {
    const userMsg: ChatMessage = {
      id: crypto.randomUUID(),
      role: "user",
      content: text,
      timestamp: new Date(),
      status: "sent",
    };
    setMessages((prev) => [...prev, userMsg]);
    // Call your agent API here and append assistant response
  };

  return (

        {messages.map((msg) => (

        ))}

  );
}
```

## FAQ

### How do I auto-resize the textarea as the user types?

Set the textarea height to `auto` on each change, then immediately set it to `scrollHeight`. Use a `useEffect` that runs when the text value changes: `ref.current.style.height = "auto"; ref.current.style.height = ref.current.scrollHeight + "px";`. Cap it with a `max-height` CSS property so it does not grow infinitely.

### Should I use a flat array or a Map for storing messages?

A flat array works well for most chat UIs under a few thousand messages. If you need frequent lookups by ID — for editing, deleting, or updating status — a `Map` paired with an ordered ID array gives O(1) lookups while preserving order. For typical agent conversations that stay under a few hundred messages, arrays are simpler and fast enough.

### How do I handle the scroll-to-bottom button that appears when the user scrolls up?

Track `isNearBottom` from the auto-scroll hook. When it is false, render a floating button at the bottom of the message container that calls `containerRef.current.scrollTo({ top: containerRef.current.scrollHeight, behavior: "smooth" })`. Hide the button when `isNearBottom` returns to true.

---

#React #ChatUI #TypeScript #Frontend #AIAgentInterface #AgenticAI #LearnAI #AIEngineering

---

Source: https://callsphere.ai/blog/building-chat-ui-react-message-bubbles-input-auto-scroll
