React UI Base
Loading...

ThreadContent

Compose thread timeline behavior with unstyled parts for empty, loading, and populated states.

ThreadContent

ThreadContent in @tambo-ai/react-ui-base owns thread timeline state derivation — messages, generation status, empty/loading detection — while keeping styling in @tambo-ai/ui-registry or your own UI layer.

Demo

No messages yet. Start a conversation!
import { ThreadContent } from "@tambo-ai/react-ui-base/thread-content";export function DemoThreadContent() {  return (    <ThreadContent.Root className="flex flex-col gap-4">      <ThreadContent.Loading className="flex items-center gap-2 p-4 text-sm text-neutral-500 dark:text-neutral-400">        <span className="inline-block h-4 w-4 animate-spin rounded-full border-2 border-neutral-300 border-t-neutral-600 dark:border-neutral-600 dark:border-t-neutral-300" />        Generating...      </ThreadContent.Loading>      <ThreadContent.Empty className="p-4 text-center text-sm text-neutral-500 dark:text-neutral-400">        No messages yet. Start a conversation!      </ThreadContent.Empty>      <ThreadContent.Messages        render={(props, state) => (          <div {...props} className="flex flex-col gap-2">            {state.filteredMessages.map((message) => (              <div                key={message.id}                className="rounded-lg border border-neutral-200 p-3 text-sm dark:border-neutral-700"              >                <span className="font-medium text-neutral-900 dark:text-neutral-100">                  {message.role}:                </span>{" "}                <span className="text-neutral-700 dark:text-neutral-300">                  {message.content.map((block) =>                    block.type === "text" ? block.text : null,                  )}                </span>              </div>            ))}          </div>        )}      />    </ThreadContent.Root>  );}

Anatomy

<ThreadContent.Root>
  <ThreadContent.Loading />
  <ThreadContent.Empty />
  <ThreadContent.Messages />
</ThreadContent.Root>

Examples

Conditional Slot Visibility

Empty and Loading unmount when their condition is false. Use keepMounted to keep them in the DOM and toggle data-hidden instead:

<ThreadContent.Empty keepMounted className="data-hidden:hidden p-4">
  Start a conversation!
</ThreadContent.Empty>

Accessing Messages via Render Props

Use the render prop on Messages to access filtered messages, loading, and empty state:

<ThreadContent.Messages
  render={(props, state) => (
    <div {...props}>
      {state.isEmpty && <p>No messages</p>}
      {state.filteredMessages.map((message) => (
        <div key={message.id}>{/* render message */}</div>
      ))}
      {state.isGenerating && <p>Generating...</p>}
    </div>
  )}
/>

Root Render State

Use the render prop on Root to access timeline state for custom layouts:

<ThreadContent.Root
  render={(props, state) => (
    <div {...props}>
      {state.isEmpty && <p>Empty thread</p>}
      {state.isGenerating && <p>Thinking...</p>}
      <p>{state.messageCount} messages</p>
    </div>
  )}
/>

API reference

Root

No custom props. Derives timeline state from Tambo hooks and provides it to children via context. Accepts render and ref via base-ui.

Render state:

FieldTypeDescription
messageCountnumberTotal number of messages in the thread.
isEmptybooleanWhether the thread has no messages.
isGeneratingbooleanWhether a response is being generated.
isLoadingbooleanWhether generating with no messages yet.

Messages

No custom props. Provides filtered messages through render state. Accepts render and ref via base-ui. System messages, empty-content messages, and standalone tool_result-only messages are automatically filtered out.

Render state:

FieldTypeDescription
messageCountnumberCount of filtered messages.
isEmptybooleanWhether filtered messages list is empty.
isGeneratingbooleanWhether a response is being generated.
filteredMessagesTamboThreadMessage[]Array of filtered messages for display.

Empty

PropTypeDefaultDescription
keepMountedbooleanfalseKeeps the node mounted and toggles data-hidden.

Renders its children only when the thread has no messages.

Render state:

FieldTypeDescription
isEmptybooleanWhether the thread has no messages.

Loading

PropTypeDefaultDescription
keepMountedbooleanfalseKeeps the node mounted and toggles data-hidden.

Renders its children only when the thread is generating and has no messages yet.

Render state:

FieldTypeDescription
isLoadingbooleanWhether generating with no messages yet.

Accessibility

  • All parts render as <div> by default. Override with the render prop for semantic markup.
  • Empty and Loading support keepMounted with data-hidden for screen-reader-friendly conditional rendering.
  • Consumers should provide appropriate aria-live attributes on regions that update dynamically.

Styling Hooks

  • data-slot="thread-content"
  • data-slot="thread-content-messages"
  • data-slot="thread-content-empty"
  • data-slot="thread-content-loading"
  • data-hidden on Empty and Loading when keepMounted is used