# Build a Custom Chat Interface
URL: /guides/build-interfaces/build-chat-interface

Tambo provides [pre-built components](https://ui.tambo.co) to help you create common conversation interfaces quickly, but if you prefer to build your own from scratch you can use the React SDK.

The Tambo React SDK provides hooks for accessing stored conversation data, allowing you to build custom interfaces that match your application's design. Whether you're building a traditional chat, a canvas-style workspace, or a hybrid interface, the SDK handles data fetching, real-time updates, and state management while you control the presentation.

This guide walks through building a complete custom conversation interface from scratch.

## Prerequisites

Before building custom conversation UI:

* Understand how [Conversation Storage](/concepts/conversation-storage) works
* Set up the `TamboProvider` in your application

## Single Conversation Interface

### Display Messages

Show the conversation history using the current thread's messages:

```tsx
import { useTambo } from "@tambo-ai/react";

export default function MessageList() {
  const { messages } = useTambo();

  return (
    <div className="messages">
      {messages.map((message) => (
        <div key={message.id} className={`message message-${message.role}`}>
          <div className="message-sender">{message.role}</div>

          {/* Render content blocks */}
          {message.content.map((content, idx) => {
            if (content.type === "text") {
              return <p key={idx}>{content.text}</p>;
            }
            if (content.type === "tool_use") {
              return (
                <div
                  key={idx}
                  className="text-sm text-gray-500 p-2 w-full text-left animate-fade-in"
                >
                  -&gt; {content.name}
                </div>
              );
            }
            if (content.type === "component" && content.renderedComponent) {
              return (
                <div key={idx} className="message-component">
                  {content.renderedComponent}
                </div>
              );
            }
            return null;
          })}
        </div>
      ))}
    </div>
  );
}
```

Messages contain an array of content blocks — text, tool calls, tool results, and components. Content blocks with `type: "component"` include a `renderedComponent` property containing any component Tambo created. Tool call blocks (`type: "tool_use"`) show which tools the AI invoked, useful for debugging or transparency.

**Alternative: Canvas-Style Display**

For interfaces showing only the latest component (dashboards, workspaces), walk backwards through messages to find the most recent component:

```tsx
import { useTambo } from "@tambo-ai/react";
import { type ReactNode } from "react";

function CanvasView() {
  const { messages } = useTambo();

  // Find the most recent rendered component by scanning forward (last wins)
  let latestComponent: ReactNode | null = null;
  for (const message of messages) {
    for (const block of message.content) {
      if (block.type === "component" && block.renderedComponent) {
        latestComponent = block.renderedComponent;
      }
    }
  }

  return (
    <div className="canvas">
      {latestComponent ? (
        latestComponent
      ) : (
        <p>Ask Tambo to create something...</p>
      )}
    </div>
  );
}
```

This pattern is useful when you want a clean workspace that updates with each AI response, rather than showing full conversation history.

### Send Messages

Create an input form that sends messages to the current thread:

```tsx
import { useTamboThreadInput } from "@tambo-ai/react";

function MessageInput() {
  const { value, setValue, submit, isPending, error } = useTamboThreadInput();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!value.trim() || isPending) return;

    await submit();
  };

  return (
    <form onSubmit={handleSubmit}>
      <textarea
        value={value}
        onChange={(e) => setValue(e.target.value)}
        placeholder="Type your message..."
        disabled={isPending}
      />
      <button type="submit" disabled={isPending || !value.trim()}>
        {isPending ? "Sending..." : "Send"}
      </button>
      {error && <div className="error">{error.message}</div>}
    </form>
  );
}
```

The `useTamboThreadInput` hook manages input state and submission, providing the current value, a setter function, a submit function, pending state, and any errors.

## Multiple Conversations

### Display Thread List

Show users their available conversations:

```tsx
import { useTamboThreadList } from "@tambo-ai/react";

export default function ThreadList() {
  const { data, isLoading, error, refetch } = useTamboThreadList();

  return (
    <div className="thread-list">
      <h2>Conversations</h2>
      {data?.threads.map((thread) => (
        <div key={thread.id} className="thread-item">
          <h3>{thread.name || "Untitled Conversation"}</h3>
          <p>{new Date(thread.createdAt).toLocaleDateString()}</p>
        </div>
      ))}
    </div>
  );
}
```

The `data.threads` array contains all stored conversations.

### Switch Between Threads

Allow users to select and view different conversations:

```tsx
import { useTambo, useTamboThreadList } from "@tambo-ai/react";

export default function ThreadList() {
  const { data, isLoading, error, refetch } = useTamboThreadList();
  const { currentThreadId, switchThread } = useTambo();

  return (
    <div className="thread-list">
      <h2>Conversations</h2>
      {data?.threads.map((thread) => (
        <div key={thread.id} className="thread-item">
          <button
            key={thread.id}
            onClick={() => switchThread(thread.id)}
            className={currentThreadId === thread.id ? "active" : ""}
          >
            {thread.name || "Untitled Conversation"}
            <p>{new Date(thread.createdAt).toLocaleDateString()}</p>
          </button>
        </div>
      ))}
    </div>
  );
}
```

When you switch threads, the entire UI automatically updates to show the new thread's messages and state. The SDK handles fetching the thread data and updating component state.

## Advanced Patterns

## Add Contextual Suggestions (Optional)

Show AI-generated suggestions after each assistant message to help users discover next actions.

### Display Suggestions

Use the `useTamboSuggestions` hook to get and display suggestions:

```tsx
import { useTambo, useTamboSuggestions } from "@tambo-ai/react";

function MessageThread() {
  const { messages } = useTambo();
  const { suggestions, isLoading, isAccepting, accept } = useTamboSuggestions({
    maxSuggestions: 3, // Optional: 1-10, default 3
  });

  const latestMessage = messages[messages.length - 1];
  const showSuggestions = latestMessage?.role === "assistant";

  return (
    <div>
      {/* Messages */}
      {messages.map((message) => (
        <div key={message.id}>
          {message.content.map((content, idx) =>
            content.type === "text" ? <p key={idx}>{content.text}</p> : null,
          )}
        </div>
      ))}

      {/* Suggestions */}
      {showSuggestions && !isLoading && (
        <div className="suggestions">
          {suggestions.map((suggestion) => (
            <button
              key={suggestion.id}
              onClick={() => accept({ suggestion })}
              disabled={isAccepting}
            >
              {suggestion.title}
            </button>
          ))}
        </div>
      )}
    </div>
  );
}
```

Suggestions are automatically generated after each assistant message when the hook is used.

### Accept Suggestions

The `accept` function provides two modes:

```tsx
// Set suggestion text in the input (user can edit before sending)
accept({ suggestion });

// Set text and automatically submit
accept({ suggestion, shouldSubmit: true });
```

## Related Concepts

* **[Conversation Storage](/concepts/conversation-storage)** - Understanding how threads are persisted
* **[Additional Context](/concepts/additional-context)** - Providing context to improve responses
* **[Component State](/concepts/generative-interfaces/component-state)** - How component state persists across renders
