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.jsonUsage
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
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | - | 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
| Prop | Type | Default | Description |
|---|---|---|---|
| title | string | "No messages yet" | Heading shown in the empty state. |
| description | string | "Start a conversation…" | Supporting text beneath the heading. |
| icon | ReactNode | - | Optional icon rendered above the title. |
| children | ReactNode | - | When provided, replaces the default title/description layout entirely. |
ConversationDownload Props
| Prop | Type | Default | Description |
|---|---|---|---|
| messages | UIMessage[] | - | The messages array from useChat. |
| filename | string | "conversation.md" | The downloaded file name. |
| formatMessage | (message, index) => string | - | Custom formatter. Defaults to bold role label + message text. |