Conversation Components
Components for building chat-style interfaces with messages, attachments, and intelligent auto-scrolling.
Core Components
Conversation
Container component that wraps the entire conversation area with auto-scroll functionality.
type ConversationProps = ComponentProps<typeof StickToBottom>;
Props:
className?: string- Additional CSS classesinitial?: "smooth" | "auto"- Initial scroll behavior (default: "smooth")resize?: "smooth" | "auto"- Scroll behavior on resize (default: "smooth")
Usage:
<Conversation className="flex-1 overflow-y-hidden">
<ConversationContent>
{/* Messages go here */}
</ConversationContent>
<ConversationScrollButton />
</Conversation>
Features:
- Uses
use-stick-to-bottomfor intelligent scrolling - Automatically scrolls to bottom when new messages arrive
- Pauses auto-scroll when user scrolls up manually
- Provides context for scroll state to child components
- Sets
role="log"for accessibility
ConversationContent
Content area for messages within the conversation.
type ConversationContentProps = ComponentProps<typeof StickToBottom.Content>;
Usage:
<ConversationContent className="flex flex-col gap-8 p-4">
{messages.map(message => (
<Message key={message.id} from={message.role}>
{/* Message content */}
</Message>
))}
</ConversationContent>
Default Styling:
- Flexbox column layout with gap
- Padding for content separation
ConversationEmptyState
Placeholder shown when there are no messages.
type ConversationEmptyStateProps = ComponentProps<"div"> & {
title?: string;
description?: string;
icon?: React.ReactNode;
};
Props:
title?: string- Heading text (default: "No messages yet")description?: string- Descriptive text (default: "Start a conversation to see messages here")icon?: React.ReactNode- Icon to display above textchildren?: React.ReactNode- Custom content (overrides default)
Usage:
{messages.length === 0 ? (
<ConversationEmptyState
title="Welcome!"
description="Ask me anything to get started"
icon={<MessageSquareIcon className="size-12" />}
/>
) : (
<ConversationContent>
{/* Messages */}
</ConversationContent>
)}
ConversationScrollButton
Button that appears when user is not at the bottom of the conversation, allowing quick navigation to latest messages.
type ConversationScrollButtonProps = ComponentProps<typeof Button>;
Usage:
<Conversation>
<ConversationContent>
{/* Messages */}
</ConversationContent>
<ConversationScrollButton />
</Conversation>
Behavior:
- Only visible when
isAtBottomis false - Positioned at bottom center of conversation
- Calls
scrollToBottom()on click - Uses
ArrowDownIconby default
Message Components
Message
Container for an individual message with role-based styling.
type MessageProps = HTMLAttributes<HTMLDivElement> & {
from: UIMessage["role"]; // "user" | "assistant"
};
Props:
from: "user" | "assistant"- Message sender roleclassName?: string- Additional CSS classes- Standard HTML div attributes
Usage:
<Message from="assistant">
<MessageContent>
<MessageResponse>{content}</MessageResponse>
</MessageContent>
<MessageActions>
<MessageAction tooltip="Copy"
<CopyIcon />
</MessageAction>
</MessageActions>
</Message>
Styling:
- User messages: right-aligned, max-width 80%
- Assistant messages: left-aligned, max-width 80%
- Adds
is-useroris-assistantclass for context-specific styling
MessageContent
Content area for message text and media.
type MessageContentProps = HTMLAttributes<HTMLDivElement>;
Usage:
<MessageContent>
<MessageResponse>{text}</MessageResponse>
</MessageContent>
Styling:
- User messages: rounded background with secondary color
- Assistant messages: plain text styling
- Flexbox column layout for multiple content types
MessageResponse
Renders markdown/text content with streaming support.
type MessageResponseProps = ComponentProps<typeof Streamdown>;
Usage:
<MessageResponse>
{message.content}
</MessageResponse>
Features:
- Uses
Streamdownfor markdown rendering - Memoized to prevent unnecessary re-renders
- Supports streaming text updates
- Removes default margin from first/last children
MessageActions
Container for action buttons (copy, edit, regenerate, etc.).
type MessageActionsProps = ComponentProps<"div">;
Usage:
<MessageActions>
<MessageAction tooltip="Copy"
<CopyIcon />
</MessageAction>
<MessageAction tooltip="Regenerate"
<RefreshIcon />
</MessageAction>
</MessageActions>
MessageAction
Individual action button with optional tooltip.
type MessageActionProps = ComponentProps<typeof Button> & {
tooltip?: string;
label?: string;
};
Props:
tooltip?: string- Tooltip text shown on hoverlabel?: string- Accessible label (falls back to tooltip)- All Button component props
Usage:
<MessageAction
tooltip="Copy to clipboard"
variant="ghost"
size="icon-sm"
>
<CopyIcon className="size-4" />
</MessageAction>
Message Branching
MessageBranch
Container for managing alternative message responses with navigation.
type MessageBranchProps = HTMLAttributes<HTMLDivElement> & {
defaultBranch?: number;
onBranchChange?: (branchIndex: number) => void;
};
Props:
defaultBranch?: number- Initial branch index (default: 0)onBranchChange?: (index: number) => void- Callback when branch changes
Usage:
<MessageBranch defaultBranch={0}
<MessageBranchContent>
<MessageResponse key="1">{response1}</MessageResponse>
<MessageResponse key="2">{response2}</MessageResponse>
<MessageResponse key="3">{response3}</MessageResponse>
</MessageBranchContent>
<MessageBranchSelector from="assistant">
<MessageBranchPrevious />
<MessageBranchPage />
<MessageBranchNext />
</MessageBranchSelector>
</MessageBranch>
Context: Provides context with:
currentBranch: number- Current branch indextotalBranches: number- Total number of branchesgoToPrevious: () => void- Navigate to previous branchgoToNext: () => void- Navigate to next branch
MessageBranchContent
Displays the current branch content, hiding others.
type MessageBranchContentProps = HTMLAttributes<HTMLDivElement>;
Behavior:
- Automatically manages branch visibility
- Updates when children change
- Preserves all branches in DOM (display: none for hidden)
MessageBranchSelector
Container for branch navigation controls.
type MessageBranchSelectorProps = HTMLAttributes<HTMLDivElement> & {
from: UIMessage["role"];
};
Behavior:
- Only renders if
totalBranches > 1 - Uses ButtonGroup for grouped appearance
MessageBranchPrevious
Button to navigate to previous branch.
type MessageBranchPreviousProps = ComponentProps<typeof Button>;
Behavior:
- Wraps around (last branch → first branch)
- Disabled if only one branch exists
- Default icon:
ChevronLeftIcon
MessageBranchNext
Button to navigate to next branch.
type MessageBranchNextProps = ComponentProps<typeof Button>;
Behavior:
- Wraps around (first branch → last branch)
- Disabled if only one branch exists
- Default icon:
ChevronRightIcon
MessageBranchPage
Displays current branch number and total.
type MessageBranchPageProps = HTMLAttributes<HTMLSpanElement>;
Display: Shows "1 of 3", "2 of 3", etc.
Attachment Components
MessageAttachment
Displays a file or image attachment with optional remove button.
type MessageAttachmentProps = HTMLAttributes<HTMLDivElement> & {
data: FileUIPart;
className?: string;
onRemove?: () => void;
};
Props:
data: FileUIPart- Attachment data (url, filename, mediaType)onRemove?: () => void- Callback to remove attachment
Usage:
<MessageAttachment
data={{
type: "file",
url: "blob:...",
filename: "document.pdf",
mediaType: "application/pdf"
}}
=> removeAttachment(id)}
/>
Behavior:
- Images: Shows thumbnail preview
- Files: Shows paperclip icon
- Hover: Shows remove button (if onRemove provided)
- Tooltip: Displays filename on non-image files
MessageAttachments
Container for multiple attachments.
type MessageAttachmentsProps = ComponentProps<"div">;
Usage:
<MessageAttachments>
{attachments.map(attachment => (
<MessageAttachment key={attachment.id} data={attachment} />
))}
</MessageAttachments>
Styling:
- Flexbox wrap layout
- Right-aligned (ml-auto)
- Gap between items
MessageToolbar
Container for toolbar elements below message content.
type MessageToolbarProps = ComponentProps<"div">;
Usage:
<MessageToolbar>
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">{timestamp}</span>
</div>
<MessageActions>
{/* Action buttons */}
</MessageActions>
</MessageToolbar>
Complete Example
import {
Conversation,
ConversationContent,
ConversationEmptyState,
ConversationScrollButton,
} from "@/components/ai-elements/conversation";
import {
Message,
MessageContent,
MessageResponse,
MessageActions,
MessageAction,
MessageAttachments,
MessageAttachment,
MessageBranch,
MessageBranchContent,
MessageBranchSelector,
MessageBranchPrevious,
MessageBranchNext,
MessageBranchPage,
} from "@/components/ai-elements/message";
function ChatInterface({ messages }: { messages: UIMessage[] }) {
return (
<Conversation className="flex-1">
{messages.length === 0 ? (
<ConversationEmptyState
title="Start a conversation"
description="Ask me anything!"
/>
) : (
<ConversationContent className="p-4">
{messages.map(message => (
<Message key={message.id} from={message.role}>
{message.attachments && (
<MessageAttachments>
{message.attachments.map(att => (
<MessageAttachment key={att.url} data={att} />
))}
</MessageAttachments>
)}
<MessageContent>
{message.branches ? (
<MessageBranch>
<MessageBranchContent>
{message.branches.map((branch, idx) => (
<MessageResponse key={idx}>{branch}</MessageResponse>
))}
</MessageBranchContent>
<MessageBranchSelector from={message.role}>
<MessageBranchPrevious />
<MessageBranchPage />
<MessageBranchNext />
</MessageBranchSelector>
</MessageBranch>
) : (
<MessageResponse>{message.content}</MessageResponse>
)}
</MessageContent>
<MessageActions>
<MessageAction tooltip="Copy" => copy(message)}>
<CopyIcon />
</MessageAction>
<MessageAction tooltip="Regenerate" => regenerate(message)}>
<RefreshIcon />
</MessageAction>
</MessageActions>
</Message>
))}
</ConversationContent>
)}
<ConversationScrollButton />
</Conversation>
);
}