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.

Install using CLI

AI Elements Vue
shadcn-vue CLI
npx ai-elements-vue@latest add conversation

Install Manually

Copy and paste the following code in the same folder.

Conversation.vue
ConversationContent.vue
ConversationEmptyState.vue
ConversationScrollButton.vue
index.ts
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@repo/shadcn-vue/lib/utils'
import { reactiveOmit } from '@vueuse/core'
import { StickToBottom } from 'vue-stick-to-bottom'

interface Props {
  ariaLabel?: string
  class?: HTMLAttributes['class']
  initial?: boolean | 'instant' | { damping?: number, stiffness?: number, mass?: number }
  resize?: 'instant' | { damping?: number, stiffness?: number, mass?: number }
  damping?: number
  stiffness?: number
  mass?: number
  anchor?: 'auto' | 'none'
}

const props = withDefaults(defineProps<Props>(), {
  ariaLabel: 'Conversation',
  initial: true,
  damping: 0.7,
  stiffness: 0.05,
  mass: 1.25,
  anchor: 'none',
})
const delegatedProps = reactiveOmit(props, 'class')
</script>

<template>
  <StickToBottom
    v-bind="delegatedProps"
    :class="cn('relative flex-1 overflow-y-hidden', props.class)"
    role="log"
  >
    <slot />
  </StickToBottom>
</template>

Usage with AI SDK

Build a simple conversational UI with Conversation and PromptInput:

Add the following component to your frontend:

pages/index.vue
<script setup lang="ts">
import { useChat } from '@ai-sdk/vue'
import { ref } from 'vue'
import {
  Conversation,
  ConversationContent,
  ConversationScrollButton,
} from '@/components/ai-elements/conversation'
import { Message, MessageContent } from '@/components/ai-elements/message'
import {
  PromptInput,
  PromptInputSubmit,
  PromptInputTextarea,
} from '@/components/ai-elements/prompt-input'
import { Response } from '@/components/ai-elements/response'

const input = ref('')
const { messages, sendMessage, status } = useChat()

function handleSubmit(e: Event) {
  e.preventDefault()
  if (input.value.trim()) {
    sendMessage({ text: input.value })
    input.value = ''
  }
}
</script>

<template>
  <div class="max-w-4xl mx-auto p-6 relative size-full rounded-lg border h-[600px]">
    <div class="flex flex-col h-full">
      <Conversation>
        <ConversationContent>
          <Message v-for="m in messages" :key="m.id" :from="m.role">
            <MessageContent>
              <Response v-for="(p, i) in m.parts" :key="i">
                {{ p.type === 'text' ? p.text : '' }}
              </Response>
            </MessageContent>
          </Message>
        </ConversationContent>
        <ConversationScrollButton />
      </Conversation>

      <PromptInput class="mt-4 w-full max-w-2xl mx-auto relative" @submit="handleSubmit">
        <PromptInputTextarea
          :value="input"
          placeholder="Say something..."
          class="pr-12"
          @input="(e: any) => (input = e?.target?.value ?? '')"
        />
        <PromptInputSubmit
          :status="status === 'streaming' ? 'streaming' : 'ready'"
          :disabled="!input.trim()"
          class="absolute bottom-1 right-1"
        />
      </PromptInput>
    </div>
  </div>
</template>

Add the following route to your backend:

server/api/chat/route.ts
import { convertToModelMessages, streamText, UIMessage } from 'ai'

// Allow streaming responses up to 30 seconds
export const maxDuration = 30

export default defineEventHandler(async (event) => {
  const { messages }: { messages: UIMessage[] } = await readBody(event)

  const result = streamText({
    model: 'openai/gpt-4o',
    messages: convertToModelMessages(messages)
  })

  return result.toAIStreamResponse(event)
})

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 class prop
  • Support for any number of child message components

Props

<Conversation />

ariaLabelstring
'Conversation'
Accessible label for the conversation container.
classstring
Additional classes applied to the container.
initialboolean | 'instant' | { damping?: number; stiffness?: number; mass?: number }
true
Controls initial stick-to-bottom behavior and spring options.
resize'instant' | { damping?: number; stiffness?: number; mass?: number }
Behavior when content resizes.
dampingnumber
0.7
Spring damping when scrolling to bottom.
stiffnessnumber
0.05
Spring stiffness when scrolling to bottom.
massnumber
1.25
Spring mass when scrolling to bottom.
anchor'auto' | 'none'
'none'
Anchoring strategy for stick-to-bottom.

<ConversationContent />

classstring
Additional classes applied to the content wrapper.

<ConversationEmptyState />

titlestring
'No messages yet'
The title text to display.
descriptionstring
'Start a conversation to see messages here'
The description text to display.
classstring
Additional classes applied to the empty state wrapper.

<ConversationScrollButton />

classstring
Additional classes applied to the scroll button.