Elements

Components / AI

Conversation

A scroll container for AI chat messages. Automatically scrolls to the bottom when new messages arrive and shows a scroll button when the user has scrolled up. Extends ai-elements Conversation.

Installation

bash
npx shadcn add https://raw.githubusercontent.com/levi-putna/elements/main/registry/conversation/registry.json

Usage

tsx
import {
  Conversation,
  ConversationContent,
  ConversationScrollButton,
  ConversationEmptyState,
} from "@/components/ui/conversation"
import { Message, MessageContent, MessageResponse } from "@/components/ui/message"

export function Chat({ messages }) {
  return (
    <Conversation>
      <ConversationContent>
        {messages.length === 0 ? (
          <ConversationEmptyState
            title="Start a conversation"
            description="Type a message below to get started."
          />
        ) : (
          messages.map((message) => (
            <Message key={message.id} from={message.role}>
              <MessageContent>
                <MessageResponse>{message.content}</MessageResponse>
              </MessageContent>
            </Message>
          ))
        )}
      </ConversationContent>
      <ConversationScrollButton />
    </Conversation>
  )
}

Examples

Full chat page with AI SDK

The most common pattern: Conversation fills available height, PromptInput sits pinned to the bottom.

tsx
import { useChat } from "ai/react"
import {
  Conversation,
  ConversationContent,
  ConversationScrollButton,
  ConversationDownload,
} from "@/components/ui/conversation"
import {
  PromptInput,
  PromptInputTextarea,
  PromptInputFooter,
  PromptInputTools,
  PromptInputSpeech,
  PromptInputSubmit,
} from "@/components/ui/prompt-input"
import { Message, MessageContent, MessageResponse } from "@/components/ui/message"

export function ChatPage() {
  const { messages, input, handleInputChange, handleSubmit, status, stop } = useChat()

  return (
    <div className="flex flex-col h-screen">
      <Conversation className="flex-1">
        <ConversationDownload messages={messages} />
        <ConversationContent>
          {messages.map((message) => (
            <Message key={message.id} from={message.role}>
              <MessageContent>
                <MessageResponse isAnimating={status === "streaming"}>
                  {message.content}
                </MessageResponse>
              </MessageContent>
            </Message>
          ))}
        </ConversationContent>
        <ConversationScrollButton />
      </Conversation>

      <div className="p-4 border-t border-border">
        <PromptInput onSubmit={() => handleSubmit()}>
          <PromptInputTextarea value={input} onChange={handleInputChange} />
          <PromptInputFooter>
            <PromptInputTools>
              <PromptInputSpeech />
            </PromptInputTools>
            <PromptInputSubmit status={status} onStop={stop} />
          </PromptInputFooter>
        </PromptInput>
      </div>
    </div>
  )
}

Custom download format

tsx
<ConversationDownload
  messages={messages}
  filename="my-conversation.md"
  formatMessage={(message, index) =>
    `### Message ${index + 1} (${message.role})\n${message.content}`
  }
/>

Sub-components

All sub-components accept standard HTML div/button props via spread.

ConversationOuter scroll container. Built on use-stick-to-bottom.
ConversationContentInner flex column. Apply gap between messages here.
ConversationEmptyStateCentred placeholder shown when messages is empty.
ConversationScrollButtonFloating arrow button. Auto-hides when scrolled to bottom.
ConversationDownloadFloating download button. Exports conversation as Markdown.

Conversation Props

PropTypeDefaultDescription
classNamestring-Additional classes on the outer scroll container.
initial"instant" | "smooth""smooth"Scroll behaviour when the component first mounts.
resize"instant" | "smooth""smooth"Scroll behaviour when the container resizes.

ConversationEmptyState Props

PropTypeDefaultDescription
titlestring"No messages yet"Heading shown in the empty state.
descriptionstring"Start a conversation…"Supporting text beneath the heading.
iconReactNode-Optional icon rendered above the title.
childrenReactNode-When provided, replaces the default title/description layout entirely.

ConversationDownload Props

PropTypeDefaultDescription
messagesUIMessage[]-The messages array from useChat.
filenamestring"conversation.md"The downloaded file name.
formatMessage(message, index) => string-Custom formatter. Defaults to bold role label + message text.