Chatbot
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.
Start a conversation
Messages will appear here as the conversation progresses.
"use client";import { Conversation, ConversationContent, ConversationEmptyState, ConversationScrollButton,} from "@/components/ai-elements/elements/conversation";import { Message, MessageAvatar, MessageContent } from "@/components/ai-elements/elements/message";import { MessageSquareIcon } from "lucide-react";import { nanoid } from "nanoid";import { useEffect, useState } from "react";const messages: { key: string; value: string; name: string; avatar: string }[] = [ { key: nanoid(), value: "Hello, how are you?", name: "Alex Johnson", avatar: "https://github.com/haydenbleasel.png", }, { key: nanoid(), value: "I'm good, thank you! How can I assist you today?", name: "AI Assistant", avatar: "https://github.com/openai.png", }, { key: nanoid(), value: "I'm looking for information about your services.", name: "Alex Johnson", avatar: "https://github.com/haydenbleasel.png", }, { key: nanoid(), value: "Sure! We offer a variety of AI solutions. What are you interested in?", name: "AI Assistant", avatar: "https://github.com/openai.png", }, { key: nanoid(), value: "I'm interested in natural language processing tools.", name: "Alex Johnson", avatar: "https://github.com/haydenbleasel.png", }, { key: nanoid(), value: "Great choice! We have several NLP APIs. Would you like a demo?", name: "AI Assistant", avatar: "https://github.com/openai.png", }, { key: nanoid(), value: "Yes, a demo would be helpful.", name: "Alex Johnson", avatar: "https://github.com/haydenbleasel.png", }, { key: nanoid(), value: "Alright, I can show you a sentiment analysis example. Ready?", name: "AI Assistant", avatar: "https://github.com/openai.png", }, { key: nanoid(), value: "Yes, please proceed.", name: "Alex Johnson", avatar: "https://github.com/haydenbleasel.png", }, { key: nanoid(), value: "Here is a sample: 'I love this product!' → Positive sentiment.", name: "AI Assistant", avatar: "https://github.com/openai.png", }, { key: nanoid(), value: "Impressive! Can it handle multiple languages?", name: "Alex Johnson", avatar: "https://github.com/haydenbleasel.png", }, { key: nanoid(), value: "Absolutely, our models support over 20 languages.", name: "AI Assistant", avatar: "https://github.com/openai.png", }, { key: nanoid(), value: "How do I get started with the API?", name: "Alex Johnson", avatar: "https://github.com/haydenbleasel.png", }, { key: nanoid(), value: "You can sign up on our website and get an API key instantly.", name: "AI Assistant", avatar: "https://github.com/openai.png", }, { key: nanoid(), value: "Is there a free trial available?", name: "Alex Johnson", avatar: "https://github.com/haydenbleasel.png", }, { key: nanoid(), value: "Yes, we offer a 14-day free trial with full access.", name: "AI Assistant", avatar: "https://github.com/openai.png", }, { key: nanoid(), value: "What kind of support do you provide?", name: "Alex Johnson", avatar: "https://github.com/haydenbleasel.png", }, { key: nanoid(), value: "We provide 24/7 chat and email support for all users.", name: "AI Assistant", avatar: "https://github.com/openai.png", }, { key: nanoid(), value: "Thank you for the information!", name: "Alex Johnson", avatar: "https://github.com/haydenbleasel.png", }, { key: nanoid(), value: "You're welcome! Let me know if you have any more questions.", name: "AI Assistant", avatar: "https://github.com/openai.png", }, ];const Example = () => { const [visibleMessages, setVisibleMessages] = useState< { key: string; value: string; name: string; avatar: string; }[] >([]); useEffect(() => { let currentIndex = 0; const interval = setInterval(() => { if (currentIndex < messages.length && messages[currentIndex]) { const currentMessage = messages[currentIndex]; setVisibleMessages((prev) => [ ...prev, { key: currentMessage.key, value: currentMessage.value, name: currentMessage.name, avatar: currentMessage.avatar, }, ]); currentIndex++; } else { clearInterval(interval); } }, 500); return () => clearInterval(interval); }, []); return ( <Conversation className="relative size-full" style={{ height: "498px" }}> <ConversationContent> {visibleMessages.length === 0 ? ( <ConversationEmptyState description="Messages will appear here as the conversation progresses." icon={<MessageSquareIcon className="size-6" />} title="Start a conversation" /> ) : ( visibleMessages.map(({ key, value, name, avatar }, index) => ( <Message from={index % 2 === 0 ? "user" : "assistant"} key={key}> <MessageContent>{value}</MessageContent> <MessageAvatar name={name} src={avatar} /> </Message> )) )} </ConversationContent> <ConversationScrollButton /> </Conversation> );};export default Example;Installation
npx ai-elements@latest add conversationnpx shadcn@latest add @ai-elements/conversation"use client";import { Button } from "@repo/shadcn-ui/components/ui/button";import { cn } from "@repo/shadcn-ui/lib/utils";import { ArrowDownIcon } from "lucide-react";import type { ComponentProps } from "react";import { useCallback } from "react";import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";export type ConversationProps = ComponentProps<typeof StickToBottom>;export const Conversation = ({ className, ...props }: ConversationProps) => ( <StickToBottom className={cn("relative flex-1 overflow-y-auto", className)} initial="smooth" resize="smooth" role="log" {...props} />);export type ConversationContentProps = ComponentProps< typeof StickToBottom.Content>;export const ConversationContent = ({ className, ...props}: ConversationContentProps) => ( <StickToBottom.Content className={cn("p-4", className)} {...props} />);export type ConversationEmptyStateProps = ComponentProps<"div"> & { title?: string; description?: string; icon?: React.ReactNode;};export const ConversationEmptyState = ({ className, title = "No messages yet", description = "Start a conversation to see messages here", icon, children, ...props}: ConversationEmptyStateProps) => ( <div className={cn( "flex size-full flex-col items-center justify-center gap-3 p-8 text-center", className )} {...props} > {children ?? ( <> {icon && <div className="text-muted-foreground">{icon}</div>} <div className="space-y-1"> <h3 className="font-medium text-sm">{title}</h3> {description && ( <p className="text-muted-foreground text-sm">{description}</p> )} </div> </> )} </div>);export type ConversationScrollButtonProps = ComponentProps<typeof Button>;export const ConversationScrollButton = ({ className, ...props}: ConversationScrollButtonProps) => { const { isAtBottom, scrollToBottom } = useStickToBottomContext(); const handleScrollToBottom = useCallback(() => { scrollToBottom(); }, [scrollToBottom]); return ( !isAtBottom && ( <Button className={cn( "absolute bottom-4 left-[50%] translate-x-[-50%] rounded-full", className )} onClick={handleScrollToBottom} size="icon" type="button" variant="outline" {...props} > <ArrowDownIcon className="size-4" /> </Button> ) );};Usage
import {
Conversation,
ConversationContent,
ConversationEmptyState,
ConversationScrollButton,
} from '@/components/ai-elements/conversation';<Conversation className="relative w-full" style={{ height: '500px' }}>
<ConversationContent>
{messages.length === 0 ? (
<ConversationEmptyState
icon={<MessageSquare className="size-12" />}
title="No messages yet"
description="Start a conversation to see messages here"
/>
) : (
messages.map((message) => (
<Message from={message.from} key={message.id}>
<MessageContent>{message.content}</MessageContent>
</Message>
))
)}
</ConversationContent>
<ConversationScrollButton />
</Conversation>Usage with AI SDK
Build a simple conversational UI with Conversation and PromptInput:
Add the following component to your frontend:
'use client';
import {
Conversation,
ConversationContent,
ConversationEmptyState,
ConversationScrollButton,
} from '@/components/ai-elements/conversation';
import { Message, MessageContent } from '@/components/ai-elements/message';
import {
Input,
PromptInputTextarea,
PromptInputSubmit,
} from '@/components/ai-elements/prompt-input';
import { MessageSquare } from 'lucide-react';
import { useState } from 'react';
import { useChat } from '@ai-sdk/react';
import { Response } from '@/components/ai-elements/response';
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 (
<Response key={`${message.id}-${i}`}>
{part.text}
</Response>
);
default:
return null;
}
})}
</MessageContent>
</Message>
))
)}
</ConversationContent>
<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:
import { streamText, UIMessage, convertToModelMessages } 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: 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
- 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 />
Prop
Type
<ConversationContent />
Prop
Type
<ConversationEmptyState />
Prop
Type
<ConversationScrollButton />
Prop
Type