• Blog
  • Docs
  • Pricing
  • We’re hiring!
Log inSign up
drewmcdonald

drewmcdonald

promptCompare

Public
Like
promptCompare
Home
Code
7
.claude
3
backend
2
docs
1
frontend
4
shared
.mcp.json
deno.json
Environment variables
2
Branches
1
Pull requests
Remixes
History
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.
Sign up now
Code
/
.claude
/
skills
/
ai-elements
/
references
/
conversation.md
Code
/
.claude
/
skills
/
ai-elements
/
references
/
conversation.md
Search
…
Viewing readonly version of main branch: v135
View latest version
conversation.md

Conversation

Wraps messages and automatically scrolls to the bottom. Also includes a scroll button that appears when not at the bottom.

The Conversation component wraps messages and automatically scrolls to the bottom. Also includes a scroll button that appears when not at the bottom.

Installation

npx ai-elements@latest add conversation

Usage with AI SDK

Build a simple conversational UI with Conversation and PromptInput:

Add the following component to your frontend:

Create val
"use client"; import { Conversation, ConversationContent, ConversationDownload, ConversationEmptyState, ConversationScrollButton, } from "@/components/ai-elements/conversation"; import { Message, MessageContent, MessageResponse, } from "@/components/ai-elements/message"; import { Input, PromptInputSubmit, PromptInputTextarea, } from "@/components/ai-elements/prompt-input"; import { MessageSquare } from "lucide-react"; import { useState } from "react"; import { useChat } from "@ai-sdk/react"; const ConversationDemo = () => { const [input, setInput] = useState(""); const { messages, sendMessage, status } = useChat(); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (input.trim()) { sendMessage({ text: input }); setInput(""); } }; return ( <div className="max-w-4xl mx-auto p-6 relative size-full rounded-lg border h-[600px]"> <div className="flex flex-col h-full"> <Conversation> <ConversationContent> {messages.length === 0 ? ( <ConversationEmptyState icon={<MessageSquare className="size-12" />} title="Start a conversation" description="Type a message below to begin chatting" /> ) : ( messages.map((message) => ( <Message from={message.role} key={message.id}> <MessageContent> {message.parts.map((part, i) => { switch (part.type) { case "text": // we don't use any reasoning or tool calls in this example return ( <MessageResponse key={`${message.id}-${i}`}> {part.text} </MessageResponse> ); default: return null; } })} </MessageContent> </Message> )) )} </ConversationContent> <ConversationDownload messages={messages} /> <ConversationScrollButton /> </Conversation> <Input onSubmit={handleSubmit} className="mt-4 w-full max-w-2xl mx-auto relative" > <PromptInputTextarea value={input} placeholder="Say something..." onChange={(e) => setInput(e.currentTarget.value)} className="pr-12" /> <PromptInputSubmit status={status === "streaming" ? "streaming" : "ready"} disabled={!input.trim()} className="absolute bottom-1 right-1" /> </Input> </div> </div> ); }; export default ConversationDemo;

Add the following route to your backend:

Create val
import { convertToModelMessages, streamText, UIMessage } from "ai"; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const result = streamText({ model: "openai/gpt-4o", messages: await convertToModelMessages(messages), }); return result.toUIMessageStreamResponse(); }

Features

  • Automatic scrolling to the bottom when new messages are added
  • Smooth scrolling behavior with configurable animation
  • Scroll button that appears when not at the bottom
  • Download conversation as Markdown
  • Responsive design with customizable padding and spacing
  • Flexible content layout with consistent message spacing
  • Accessible with proper ARIA roles for screen readers
  • Customizable styling through className prop
  • Support for any number of child message components

Props

<Conversation />

PropTypeDefaultDescription
contextRefReact.Ref<StickToBottomContext>-Optional ref to access the StickToBottom context object.
instanceStickToBottomInstance-Optional instance for controlling the StickToBottom component.
children`((context: StickToBottomContext) => ReactNode)ReactNode`-
...propsOmit<React.HTMLAttributes<HTMLDivElement>,-Any other props are spread to the root div.

<ConversationContent />

PropTypeDefaultDescription
children`((context: StickToBottomContext) => ReactNode)ReactNode`-
...propsOmit<React.HTMLAttributes<HTMLDivElement>,-Any other props are spread to the root div.

<ConversationEmptyState />

PropTypeDefaultDescription
titlestring-The title text to display.
descriptionstring-The description text to display.
iconReact.ReactNode-Optional icon to display above the text.
childrenReact.ReactNode-Optional additional content to render below the text.
...propsComponentProps<-Any other props are spread to the root div.

<ConversationScrollButton />

PropTypeDefaultDescription
...propsComponentProps<typeof Button>-Any other props are spread to the underlying shadcn/ui Button component.

<ConversationDownload />

A button that downloads the conversation as a Markdown file.

Create val
import { ConversationDownload } from "@/components/ai-elements/conversation"; <Conversation> <ConversationContent> {messages.map(...)} </ConversationContent> <ConversationDownload messages={messages} /> <ConversationScrollButton /> </Conversation>
PropTypeDefaultDescription
messagesConversationMessage[]RequiredArray of messages to include in the download.
filenamestring-The filename for the downloaded file.
formatMessage(message: ConversationMessage, index: number) => string-Custom function to format each message in the output.
...propsOmit<ComponentProps<typeof Button>,-Any other props are spread to the underlying shadcn/ui Button component.

messagesToMarkdown

A utility function to convert messages to Markdown format. Useful for custom download implementations.

Create val
import { messagesToMarkdown } from "@/components/ai-elements/conversation"; const markdown = messagesToMarkdown(messages); // With custom formatter const customMarkdown = messagesToMarkdown( messages, (msg, i) => `[${msg.role}]: ${msg.content}`, );
FeaturesVersion controlCode intelligenceCLIMCP
Use cases
TeamsAI agentsSlackGTM
DocsShowcaseTemplatesNewestTrendingAPI examplesNPM packages
PricingNewsletterBlogAboutCareers
We’re hiring!
Brandhi@val.townStatus
X (Twitter)
Discord community
GitHub discussions
YouTube channel
Bluesky
Open Source Pledge
Terms of usePrivacy policyAbuse contact
© 2026 Val Town, Inc.